Posted on 11/11/2008 9:16:26 PM by Justin Etheredge
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
Part 3 Generic Covariance
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.