Search code examples
c#polymorphismcovariance

Why is a generic implementing an interface not covariant, but a generic of a base class is?


I noticed while trying to implement a generic that there was different behavior between a class with a generic that implements an interface vs a class with a generic that extends a Base class. With the interface, I can't call a function that takes an Enumerable of the interface type, but with the class everything works just fine. Here's an example

public interface IBarInterface
{
    public int Value { get; set; }
}

public class FooInterface<TInterface> where TInterface : IBarInterface
{
    private List<TInterface> _items;

    public List<TInterface> Items => _items;

    // Does not compile:
    //  Argument type 'System.Collections.Generic.List<TInterface>' is not assignable to parameter type 'System.Collections.Generic.IEnumerable<IBarInterface>'
    public bool SomeValue => Processors.DoSomethingInterface(_items);

    public FooInterface()
    {
        _items = new List<TInterface>();
    }
}

public class BarClass
{
    public int Value { get; set; }
}

public class FooClass<TClass> where TClass : BarClass
{
    private List<TClass> _items;

    public List<TClass> Items => _items;

    // Does compile
    public bool SomeValue => Processors.DoSomethingClass(_items);

    public FooClass()
    {
        _items = new List<TClass>();
    }
}

public static class Processors
{
    public static bool DoSomethingInterface(IEnumerable<IBarInterface> items)
        => items.Count() % 2 == 0;

    public static bool DoSomethingClass(IEnumerable<BarClass> items)
        => items.Count() % 2 == 0;
}

FooInterface fails to compile, but FooBar compiles just fine. Why is this the case?


Solution

  • The crucial difference between interfaces and classes in this case, is that structs can implement interfaces too! Covariance/contravariance conversions, like converting from IEnumerable<Subtype> to IEnumerable<Supertype>, is only available if Subtype and Supertype are both reference types.

    In the case of FooClass<TClass>, TClass is constrained to be a subclass of BarClass, so TClass has to be a reference type.

    In the case of FooInterface<TInterface>, TInterface is only constrained to be an implementation of IBarInterface, so it can be struct too. There is no guarantee that a covariance conversion is valid at DoSomethingInterface(_items).

    So if you just ensure that TInterface cannot be a value type,

    where TInterface : class, IBarInterface
    

    then the error will be gone.