The IComparable<in T>
interface is defined as Contra-Variance.
Contra-Variance wrote the following code to check what constraint there is.
public class Parent : IComparable<Parent>
{
public string Name { get; set; }
public int CompareTo(Parent other) =>
string.Compare(Name, other.Name, StringComparison.Ordinal);
}
public class Child : Parent
{
}
public static void Compare(IComparable<Child> comparable, Child target)
{
var result = comparable.CompareTo(target);
}
IComparable<Parent> parentComparable = new Parent {Name = "Parent"};
var child = new Child {Name = "Child"};
Compare(parentComparable, child);
Code that receives IComparable<Parent>
as an IComparable<Child>
. It shows typical Contra-Variance characteristics.
But suddenly, this thought occurred to me.
Can't I pass an IComparable<Child>
as an IComparable<Parent>
? Of course, It's not possible because IComparable<in T>
is Contra-Variance. But, if possible, I wondered, is there a logical problem?
There seems to be no problem in my opinion.
So why doesn't C# Compiler allow it? Maybe am I wrong?
The logical problem is very obvious if you try to construct an example like this:
public class Parent {
}
public class Child: Parent, IComparable<Child> {
public int SomethingSpecificToChild { get; }
public int CompareTo(Child other) => SomethingSpecificToChild.CompareTo(other.SomethingSpecificToChild);
}
public class Program {
public static void Main(string[] args) {
ExpectsComparableParent(new Child());
}
public static void ExpectsComparableParent(IComparable<Parent> parent) {
parent.CompareTo(new Parent());
}
}
Here I am passing new Child()
as the parameter of ExpectsComparableParent
, which expects a IComparable<Parent>
. If this did compile, then at runtime the parent.CompareTo
call would resolve to the CompareTo
declared in Child
, and the line
SomethingSpecificToChild.CompareTo(other.SomethingSpecificToChild);
would run. However, the parameter other
is passed an argument of new Parent()
, which doesn't actually have a SomethingSpecificToChild
property.
Another way to see this is to list what IComparable<Parent>
and IComparable<Child>
can do.
IComparable<Parent>
can be compared with any instance of Parent
or any instance of Child
, since you can pass an instance of Child
into IComparable<Parent>.CompareTo
.
IComparable<Child>
can only be compared with any instance of Child
.
Clearly, IComparable<Parent>
can do everything that IComparable<Child>
can do, and more, hence an instance of IComparable<Parent>
can be converted to IComparable<Child>
, but not vice versa.
It’s also worth noting that contravariant types’ subtyping relationship being the opposite of the subtyping relationship of the their type parameters is exactly what the “contra-“ in “contravariance” is referring to :)