Search code examples
c#contravariance

Why does contravariance only allow the reverse of assign compatible?


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?


Solution

  • 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 :)