Sunday, May 10, 2009

Co-variance / Contra-variance - Part I

Apparently, one common mistake made by C# programmers involves co-variance and contra-variance. Among the cross-thread operation not valid exception and inter-form communication, one of the most common questions seen on the Microsoft's Visual C# Forum is why a List<MyDerivedType> can't be cast to a List<MyBaseType>. I must confess that the first time encountered this problem I was both surprised and pissed. In time I learned that when the language doesn't allow you to perform some kind of action, there is a good reason behind it. Soon enough I realized that this wasn't the exception.

Take into consideration the following code:

public class MyBase
{
    public virtual void Operation()
    {
        // Some code.
    }
}

public class MyFirstDerived : MyBase
{
    public override void Operation()
    {
        // Some code.
    }
}

public class MySecondDerived : MyBase
{
    public override void Operation()
    {
        // Some code.
    }
}

class Program
{
    static void Main(string[] args)
    {
        IList<MyFirstDerived> myStronglyTypedList = new List<MyFirstDerived>();
        BlowUpMethod(myStronglyTypedList);
        SafeMethod(myStronglyTypedList);
    }

    static void BlowUpMethod(IList<MyBase> aNotSoStronglyTypedList)
    {
        aNotSoStronglyTypedList.Add(new MySecondDerived());
    }

    static void SafeMethod(IList<MyBase> aNotSoStronglyTypedList)
    {
        foreach (MyBase aBaseObject in aNotSoStronglyTypedList)
        {
            aBaseObject.Operation();
        }
    }
}

Since C#, up to version 3.0, is invariant this code does not compile. One of the errors states:

Argument '1': cannot convert from 'System.Collections.Generic.IList<CoVarianceContraVariance.MyFirstDerived>' to 'System.Collections.Generic.IList<CoVarianceContraVariance.MyBase>'

If it did work, in SafeMethod there would no issues at all. This would work without any undesired secondary effects. This behavior is called co-variance. On the other hand, after invoking the BlowUpMethod we would have a MySecondDerived instance in a list were only MyFirstDerived (and derivations) instances are allowed. This will defeat the whole purpose of having strongly typed lists, wouldn't it?

Fortunately, there is a workaround to allow some sort of co-variance behavior that I find extremely useful in some scenarios.

static void SafeMethod<T>(IList<T> aStronglyTypedList) where T : MyBase
{
    foreach (MyBase aBaseObject in aStronglyTypedList)
    {
        aBaseObject.Operation();
    }
}

Works like a charm!

As you would expect a similar problems occurs when trying to apply contra-variance, but it is in fact a little trickier:

public abstract class Animal {}
public abstract class Carnivore : Animal
{
    public void Hunt (Animal prey)
    {
        // Some code.
    }
}
public class Tiger : Carnivore { }
public abstract class Herbivore : Animal { }
public class Antelope : Herbivore { }

public class Program
{
    static void Main(string[] args)
    {
        List<Animal> animals = new List<Animal>()
        {
            new Antelope(),
            new Tiger()
        };
        HuntingOverkill(animals, new Antelope());
        AddTigerToPack(animals);
    }

    static void HuntingOverkill(List<Carnivore> packOfCarnivores, Animal prey)
    {
        foreach (Carnivore carnivore in packOfCarnivores)
        {
            carnivore.Hunt(prey);
        }
    }

    static void AddTigerToPack(List<Tiger> registeredTigers)
    {
        registeredTigers.Add(new Tiger());
    }
}

Again, this code will not compile since a list of animals cannot be converted to a list of tigers. If it did, the method HuntingOverkill would be making a defenseless antelope hunt, and that is why this isn't allowed, although the method AddTigerToPack could work without any problems.

A similar workaround could be tried here, but it wouldn't work either, since if we made registeredTigers a list of T with T inheriting from Animal, we could end up with a Tiger inside a pack of herbivores. Those poor little creatures, can you imagine the chaos? So I tried to work up some workaround for this too, but although I tried banging my head with the keyboard, I couldn't come up with one that looked cool. I only ended up with ugly methods with generic arguments with lots of constraints and that couldn't be inferred by the compiler when calling them.

All this suffering will be finally put to an end once C# 4.0 comes out with the lovely new feature of safe Co- and Contra-variance. I will elaborate on how it will work on my next post.

Random thoughts: One could possible ask why the C# compiler isn't smart enough to allow cases like SafeMethod or AddTigerToPack. Well, first of all, I wouldn't like the compiler looking so deep into my code before compiling. A line has to be drawn at some point and I think this goes far beyond that line. Also this example is pretty simple and this cases of variance can (and will) end up in much more complex code. It will end up making the compiler having to do an awful lot of work and it would take huge amount of time in big projects. Finally and most importantly, changing something that is completely legal inside the method's scope would introduce breaking changes! It can even cause an application using our class library to stop working. So no, this is completely out of the question.

Follow-up: Co-variance / Contra-variance - Part II.

No comments:

Post a Comment