C# 4.0 New Features Part 3 – Generic Covariance

Writing

Here are the previous parts to this series:

Part 1 – Dynamic Keyword

Part 1.1 – Dynamic Keyword Second Look

Part 2 – Default And Named Parameters

Part 2.1 Default Parameter Intrigue

When generics were introduced in C# 2.0 they were one of the best features that ever came to C#. Anyone who had to create strongly typed collection classes in C# 1.0 knows exactly how much code generics saved us from having to write. The problem though is that generics don’t seem to follow the same rules of inheritance that all of the other classes follow. Let’s start off by defining two quick classes that we are going to use for the rest of this post:

public class Shape
{
}

public class Circle : Shape
{
}

Here we have our stereotypical class hierarchy, which is not doing anything currently. But the behavior of these classes is not important. Now, lets define a dummy container class that can hold an instance of any class:

public interface IContainer<T>
{
    T GetItem();
}

public class Container<T>: IContainer<T> 
{
    private T item;

    public Container(T item)
    {
        this.item = item;
    }

    public T GetItem()
    {
        return item;
    }
}

Now that we have our hierarchy and our container class, let’s look at something that we can’t currently do in C# 3.0:

static void Main(string[] args)
{            
    IContainer<Shape> list = GetList();
}

public static IContainer<Shape> GetList()
{
    return new Container<Circle>(new Circle());
}

We have a method called “GetList” which has a return type of “IContainer<Shape>” and then returns a “Container<Circle>” class. Since Circle descends from Shape and Container implements IContainer, you would think think that this would just work. But in C# 3.0, it doesn’t.

In C# 4.0, we have a way to make this work, we can simply add the word “out” in front of the type parameter on our interface declaration (note that variance in C# 4.0 is limited to interfaces and delegate types):

public interface IContainer<out T>
{
    T GetItem();
}

This is telling the C# compiler that T is covariant, which means that any IContainer<T> will accept any type equal to or more specific than T. Like we saw above, IContainer<Shape> was the return type, but if we have the out parameter on our interface, then we had no problem returning an IContainer<Circle>.

So why did they decide to use the word “out”? Well, it is because whenever you define a type parameter as covariant, you can only return that type out of the interface. For example, this is invalid:

public interface IContainer<out T>
{
    void SetItem(T item);
    T GetItem();
}

But why won’t that work? Because if that doesn’t work, then that means that the IList<T> interface can’t be covariant! Noooo! Well, the reason is actually pretty simple, type safety. Let’s look at the implications of what we have done above:

static void Main(string[] args)
{
    IContainer<Shape> container = new Container<Circle>();
    SetItem(container);
}

public static void SetItem(IContainer<Shape> container)
{
    container.SetItem(new Square()); // BOOM!!!
}

You see that since T is covariant and so we can assign a “Container<Circle>” to our variable of type “IContainer<Shape>” and then we pass it into our method “SetItem” which accepts a parameter of type “IContainer<Shape>” and then we take that variable and try to add a new type “Square” to it. Well, it looks like this is valid, the parameter type is “IContainer<Shape>” and so we should be able to add a Square, right? Well, wrong. The line above will explode because we are actually trying to add a square to a container that holds circles. This is why they limited covariance to only a single direction.

Are you wondering how all of this is implemented in the clr? Well, there is no need to. Generic covariance in the clr is the way that it just works. Since generics were worked into the clr in .net 2.0 they have allowed this behavior. Since C# tries its best to maintain type safety, they didn’t allow what we just did above. The clr though has no problem with it. As an interesting side note, arrays in C# actually allow this behavior, so go try it out! I hope that you enjoyed this post, and then next in the series will be here soon!

Loved the article? Hated it? Didn’t even read it?

We’d love to hear from you.

Reach Out

Leave a comment

Leave a Reply

Your email address will not be published.

More Insights

View All