Search code examples
c#.netgenericscovariance

Why can't I assign a List<Derived> to a List<Base>?


I defined the following class:

public abstract class AbstractPackageCall
    {

     ...

    }

I also define a subclass of this class:

class PackageCall : AbstractPackageCall
    {

      ...
    }

There are also several other subclases of AbstractPackageCall

Now I want to make the following call:

 List<AbstractPackageCall> calls = package.getCalls();

But I always get this exception:

Error   13  Cannot implicitly convert type 'System.Collections.Generic.List<Prototype_Concept_2.model.PackageCall>' to 'System.Collections.Generic.List<Prototype_Concept_2.model.AbstractPackageCall>' 

What is the problem here? This is the method Package#getCalls

 internal List<PackageCall> getCalls()
        {
            return calls;
        }

Solution

  • The simplest way to understand why this is not allowed is the following example:

    abstract class Fruit
    {
    }
    
    class Apple : Fruit
    {
    }
    
    class Banana : Fruit
    {
    }
    
    // This should intuitively compile right? Cause an Apple is Fruit.
    List<Fruit> fruits = new List<Apple>();
    
    // But what if I do this? Adding a Banana to a list of Apples
    fruits.Add(new Banana());
    

    The last statement would ruin the type safety of .NET.

    Arrays however, do allow this:

    Fruit[] fruits = new Apple[10]; // This is perfectly fine
    

    However, putting a Banana into fruits would still break type safety, so therefor .NET has to do a type check on every array insertion and throw an exception if it's not actually an Apple. This is potentially a (small) performance hit, but this can be circumvented by creating a struct wrapper around either type as this check does not happen for value types (because they can't inherit from anything). At first, I didn't understand why this decision was made, but you'll encounter quite often why this can be useful. Most common is String.Format, which takes params object[] and any array can be passed into this.

    In .NET 4 though, there's type safe covariance/contravariance, which allows you to make some assignments like these, but only if they're provably safe. What's provably safe?

    IEnumerable<Fruit> fruits = new List<Apple>();
    

    The above works in .NET 4, because IEnumerable<T> became IEnumerable<out T>. The out means that T can only ever come out of fruits and that there's no method at all on IEnumerable<out T> that ever takes T as a parameter, so you can never incorrectly pass a Banana into IEnumerable<Fruit>.

    Contravariance is much the same but I always forget the exact details on it. Unsurprisingly, for that there's now the in keyword on type parameters.