Search code examples
c#.netcovariancecontravariance

Why the concept of "Covariance" and "Contravariance" are applicable while implementing the methods of an interface?


The use case is some what like this:

public class SomeClass : ICloneable
{
    // Some Code

    // Implementing interface method
    public object Clone()
    {
        // Some Clonning Code
    }
}

Now my question is Why is it not possible to use "SomeClass(As it is derived from object)" as a return type of Clone() method if we consider the Funda's of Covariance and Contravariance?

Can somebody explain me the reason behind this implementation of Microsoft ????


Solution

  • A non-broken implementation of interface-implementation variance would have to be covariant in the return type and contravariant in the argument types.

    For example:

    public interface IFoo
    {
        object Flurp(Array array);
    }
    
    public class GoodFoo : IFoo
    {
        public int Flurp(Array array) { ... }
    }
    
    public class NiceFoo : IFoo
    {
        public object Flurp(IEnumerable enumerable) { ... }
    }
    

    Both are legal under the "new" rules, right? But what about this:

    public class QuestionableFoo : IFoo
    {
        public double Flurp(Array array) { ... }
        public object Flurp(IEnumerable enumerable) { ... }
    }
    

    Kind of hard to tell which implicit implementation is better here. The first one is an exact match for the argument type, but not the return type. The second is an exact match for the return type, but not the argument type. I'm leaning toward the first, because whoever uses the IFoo interface can only ever give it an Array, but it's still not entirely clear.

    And this isn't the worst, by far. What if we do this instead:

    public class EvilFoo : IFoo
    {
        public object Flurp(ICollection collection) { ... }
        public object Flurp(ICloneable cloneable) { ... }
    }
    

    Which one wins the prize? It's a perfectly valid overload, but ICollection and ICloneable have nothing to do with each other and Array implements both of them. I can't see an obvious solution here.

    It only gets worse if we start adding overloads to the interface itself:

    public interface ISuck
    {
        Stream Munge(ArrayList list);
        Stream Munge(Hashtable ht);
        string Munge(NameValueCollection nvc);
        object Munge(IEnumerable enumerable);
    }
    
    public class HateHateHate : ISuck
    {
        public FileStream Munge(ICollection collection);
        public NetworkStream Munge(IEnumerable enumerable);
        public MemoryStream Munge(Hashtable ht);
        public Stream Munge(ICloneable cloneable);
        public object Munge(object o);
        public Stream Munge(IDictionary dic);
    }
    

    Good luck trying to unravel this mystery without going insane.

    Of course, all of this is moot if you assert that interface implementations should only support return-type variance and not argument-type variance. But almost everyone would consider such a half-implementation to be completely broken and start spamming bug reports, so I don't think that the C# team is going to do it.

    I don't know if this is the official reason why it's not supported in C# today, but it should serve as a good example of the kind of "write-only" code that it could lead to, and part of the C# team's design philosophy is to try to prevent developers from writing awful code.