Here are the previous parts to this series:
Part 1.1 – Dynamic Keyword Second Look
Part 2 – Default And Named Parameters
Part 2.1 Default Parameter Intrigue
I went on a serious blogging streak, and then all the sudden I just dropped off with my C# 4.0 new features series. I guess that just goes to show you that I need to spread this stuff out! Anyways, I am back today with part 4 of this series on generic contravariance. This post is actually probably going to be pretty short, because contravariance is a topic that is very similar to covariance, but it also isn’t really all that interesting. It can be quite useful though, and that is why I am bringing it to you here.
In the last post we created an interface that looks like this:
public interface IContainer<out T> { T GetItem(); }
With this interface we can now say that IContainer is covariant on T, which as explained previously, means that this type can now return objects of Type T and anything more specific than T (subclasses). But what does it mean for something to be contravariant? Well, it is probably easier to show it than it is to explain it. First we can take a look at our class which implements the code above:
public class Container<T> : IContainer<T> { private T item; public Container(T item) { this.item = item; } public T GetItem() { return item; } }
We have our container class above, and we can pass in our item in the constructor and use it like this:
Circle circle = new Circle(); IContainer<Shape> container = new Container<Circle>(circle);
So you see that we are declaring a class of type Circle and then passing that into our Container class which is being assigned to the IContainer<Shape>. Here we are seeing Covariance in action. But what happens if we want to perform some action on the item that our container is holding? We could add a method like this to our interface:
public interface IContainer<out T> { T GetItem(); void Do(Action<T> action); }
This won’t work though. Well, why not? The issue is that Action is not contravariant on T (it will be in the .net 4.0 release, but it is not yet). Since Action is not contravariant on T, if we declare IContainer<Shape> then the Action delegate would need to be able to accept type T and anything more specific than it.
In order to do this, we first need to declare a new Action delegate type:
public delegate void ContraAction<in T>(T a);
So you see that our keyword for contravariance is “in”, since these types can only be passed in to a method. Now we can define our interface like this:
public interface IContainer<out T> { T GetItem(); void Do(ContraAction<T> action); }
And so if we declare our container class like this:
public class Container<T> : IContainer<T> { private T item; public Container(T item) { this.item = item; } public T GetItem() { return item; } public void Do(ContraAction<T> action) { action(item); } }
We can then use it like this:
Circle circle = new Circle(); IContainer<Shape> container = new Container<Circle>(circle); container.Do(s => s.MethodCallOnShape());
Pretty sweet, huh? So we have a container that can hold a Circle, Square, Triangle, etc… which can then take a delegate into a method that will perform an action on the base “Shape” type, but will accept any shape as a parameter.
Loved the article? Hated it? Didn’t even read it?
We’d love to hear from you.
Just a quick note – the "Part 3" link in the article points to the clarification on default method values. The "related links" has it however.
Thanks for the thoughtful article, and please keep up the good work.
-Scott
The link in your article to C# 4.0 Features Part 3 is linking to Part 2.1 🙂
@Scott and @Adam Thanks, it is fixed now.
I don’t buy what the difference is between normal generics?
And why can’t we just use something like:
public interface IContainer<T>
{
T GetItem();
}
Maybe I just should read with more attention…
Cheers.
Generics in C# 2.0 and 3.0 are not variant. Meaning you cannot assign a List<Dog> to a List<Animal> and vice versa. Intuitively this should work, but in practice there are several reasons why this doesn’t work well with strong typing. In C# 4.0 generics will have the ability to do something similar to what I stated above, but using interfaces.