Thursday 23 December 2010

Covariance and Contravariance

My company uses Wintellect and I was fortunate enough to listen in to an open ended Q&A session with John Robbins and Jeffrey Richter. I was surprised at how underwhelmed Jeffrey was with C# 4.0 and with his statement that apart from the stuff added for COM interop there was also generic "covariance" and "contravariance" (from now on I'll sometimes use the collective term "variance" when referring to both), but that "know one really cared about that anyway".  I was aware of these terms and thought I'd see what it was all about so I could make my own mind up.  I hunted around for some examples of variance, but couldn't find a step-by-step guide that explained it simply.  Eric Lippert has written the definitive blog postings on variance, but his explanations are not for the faint-hearted
http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/default.aspx.

Hopefully this guide explains the concepts as simply as possible.

Ok, C# 4.0 introduces Covariance and Contravariance for generic interface and delegate types.

But what do the terms mean?
Consider two classes BaseClass and DerivedClass:
class BaseClass
{
}

class DerivedClass : BaseClass
{
}

I've seen plenty of blogs and postings that suggest that if you can assign an object of DerivedClass to an object of type BaseClass that is covariance and if you can assign an object from BaseClass to DerivedClass then that is contravariance. This is not really true, when you assign an object of type DerivedClass to an object of type BaseClass that is called assignment compatibility.  Covariance and Contravariance are terms with more specific meanings than simple assignment compatibility. Seeing as C# 4.0 introduced covariance and contravariance for generic interface and delegate types let's explore what each of these mean:

Variance in Delegates
"Covariance and contravariance provide a degree of flexibility when you match method signatures with delegate types. Covariance permits a method to have a more derived return type than what is defined in the delegate. Contravariance permits a method with parameter types that are less derived than in the delegate type."

This has nothing to do with generics or with .NET 4.0!! In fact this variance has been around since .NET 2.0. From my point of view it was only the fact that it was featured as being introduced in .NET 4.0 for generic interfaces and delegates that alerted me to the fact that variance has been kicking around for non-generic delegates in .NET for quite some time already. So a good starting point is to understand variance from a .NET 2.0 perspective.
In the code below I have two really simple classes with DerivedClass deriving from BaseClass. I also have a class called DelegatesAndHandlers which has four delegates defined at the top and a number of methods with varying return types or parameter types. Again, just to reiterate none of this code is generic or requires .NET 4.0

namespace VarianceExamples
{
    class BaseClass
    {
        public bool BaseState { getset; }
    }

    class DerivedClass : BaseClass
    {
        public bool DerivedState { getset; }
    }
 
    class DelegatesAndHandlers
    {
        delegate BaseClass ReturnsBaseClass();
        delegate DerivedClass ReturnsDerivedClass();
        delegate void ReceivesDerivedClass(DerivedClass dc);
        delegate void ReceivesBaseClass(BaseClass bc);
 
        static DerivedClass ReturnsDerivedClassHandler()
        {
            Console.WriteLine("In ReturnsDerivedClassHandler");
            return new DerivedClass();
        }

        static BaseClass ReturnsBaseClassHandler()
        {
            Console.WriteLine("In ReturnsBaseClassHandler");
            return new BaseClass();
        }
 
        static void ReceivesBaseClassHandler(
        BaseClass bc)
        {
            Console.WriteLine("In ReceivesBaseClassHandler");
        }
 
        static void ReceivesDerivedClassHandler(
        DerivedClass dc)
        {
            Console.WriteLine("In ReceivesDerivedClassHandler");
        }
}

Given the delegates and handlers above I should clearly be able to do the following:

ReturnsBaseClass target = ReturnsBaseClassHandler;
target.Invoke();


That's not variance - it's just code!

Covariance
What may be less obvious is that I can also do this:

ReturnsBaseClass coVariantTarget = ReturnsDerivedClassHandler;
coVariantTarget.Invoke();

The ReturnBaseClass delegate defines that a BaseClass object will be returned but the handler actually returns a DerivedClass. This is fine and type-safe because all the state in the returned object will have been initialised – This is an example of delegate covariance.

What I cannot do is the opposite:

//ReturnsDerivedClass nonono = ReturnsBaseClassHandler;
//nonono.Invoke();

This won't even compile – I cannot promise (in the delegate) to hand back a DerivedClass and then hand back a BaseClass instance because the DerivedClass, if it could be returned, would contain uninitialised state; the conversion is not type-safe.

We can do away with the explicit code handlers if we use the .NET 3.5 lambda equivalents:

ReturnsBaseClass noSurprise = ()
   => { return new BaseClass(); };
ReturnsBaseClass demonstrateCoVariance = ()
   => { return new DerivedClass(); };

And we can then do the following; we can set a delegate to its covariant version:

noSurprise = demonstrateCoVariance;

So that's delegate covariance – what about delegate contravariance?

Contravariance
Well, similar to the covariance example we'll start with the given:

ReceivesBaseClass target = ReceivesBaseClassHandler;
target.Invoke(new BaseClass());
  

The contravariant example is shown below:

ReceivesDerivedClass contraVariantTarget = ReceivesBaseClassHandler;
contraVariantTarget.Invoke(new DerivedClass());
      

In this case, our delegate is typed to receive a BaseClass but we are sending it a DerivedClass – again, this is fine and type-safe. All state in BaseClass has been initialised.

Now look at the code below. We cannot do the reverse because we cannot promise a DerivedClass and then send a BaseClass in. If the handler was to try to use the object, what would happen if it tried to access DerivedClass state?
Therefore the following code will not compile:

//ReceivesBaseClass nonono = ReceivesDerivedClassHandler;
//nonono.Invoke(new BaseClass());     

Again, we can use anonymous delegates to reduce the amount of code:

ReceivesDerivedClass alsoNoSurprise = (DerivedClass dc) => { };

The above is a terse rewrite of the obvious case, if we then try to rewrite the covariant example as a lambda:

ReceivesDerivedClass demonstrateContraVariance =
   (BaseClass bc) => { };            

We get the following error message:

"Cannot convert lambda expression to delegate type 'VarianceExamples.DelegatesAndHandlers.ReceivesDerivedClass' because the parameter types do not match the delegate parameter types

This has nothing to do with this blog posting really, but I thought it was worthy of mention – Contravariance does not work with anonymous methods and the explanation is provided here:


So, just as we could set a delegate to its covariant version we can also set a delegate to its contravariant version. (Except we can't do this using anonymous delegates as we've just discovered, so we'll go back to using our "ReceivesBaseClassHandler" named handler to instantiate the delegate):

ReceivesDerivedClass contraVariantTarget = ReceivesBaseClassHandler;
alsoNoSurprise = contraVariantTarget;

To generics and C# 4.0
With reference to delegate variance all that .NET 4.0 does is make generic delegates behave like non-generic ones – that's it really!

Generic Delegate Covariance
To explore this let's go and define a generic delegate:

delegate TReturn ReturnClass<TReturn>();

We can then create a couple of anonymous delegate instances:

ReturnClass<BaseClass> noSurpriseGenericOutBase = ()
   => { return new BaseClass(); };

ReturnClass<DerivedClass> noSurpriseGenericOutDerived = ()
   => { return new DerivedClass(); };

Given what we now know about covariance we might reasonably expect to be able to do the following:

noSurpriseGenericOutBase = noSurpriseGenericOutDerived;

But if we do that we get the following error message:

Cannot implicitly convert type 'VarianceExamples.DelegatesAndHandlers.ReturnClass<VarianceExamples.DerivedClass>' to VarianceExamples.DelegatesAndHandlers.ReturnClass<VarianceExamples.BaseClass>'

The fix that we get with .NET 4.0 is that we can insert the out keyword in the generic parameter definition:

delegate TReturn ReturnClass<out TReturn>();

Generic Delegate Contravariance
If we also go and define another generic delegate:

delegate void ReceiveClass<TParam>(TParam param);

We can then create a couple of anonymous delegate instances:

ReceiveClass<BaseClass> noSurpriseGenericInBase =
   (BaseClass bc) => { };
ReceiveClass<DerivedClass> noSurpriseGenericInDerived =
   (DerivedClass dc) => { };

Given what we also now know about contravariance we might reasonably expect to be able to do the following:

noSurpriseGenericInDerived =  noSurpriseGenericInBase;

But if we do that we get another error message.  The fix that we get with .NET 4.0 is that we can insert the in keyword in the generic parameter definition:

delegate void ReceiveClass<in TParam>(TParam param);

And that's it for C# 4.0 and generic delegate covariance and contravariance! We can make a generic delegate covariant by adding out to the return parameter type and we can make a generic delegate contravariant by adding the in keyword to one of the generic parameters. Then they behave like their non-generic counterparts.

Generic Interface Variance
After all that lot you'll be relieved to know that the explanation for generic interface variance is not as difficult:

Covariance
Some generic interface types in C# 4.0 are now marked as being covariant (note the out keyword):

From mscorlib: v4.xx
public interface IEnumerable<out T> : IEnumerable

IEnumerable is defined to be covariant - if you peeked into previous versions of mscorlib you would not see the out keyword.  So, you can now do the following in C# 4.0:

IEnumerable<DerivedClass> dcList = new List<DerivedClass>();
IEnumerable<BaseClass> bcList = dcList;

In other words you can assign a more derived collection to a less derived one. This is fine and type-safe because consuming code is always getting values out of these objects. If you try this in previous versions of .NET it will not compile.

Contravariance
Some delegate types are also marked as being contravariant (note the in keyword):

From mscorlib: v4.xx
public delegate void Action<in T>(T obj);

So, you can also now do the following in C# 4.0:

Action<BaseClass> bcAction = delegate(BaseClass bc) { };
Action<DerivedClass> dcAction = bcAction;


In other words you can convert from Action<BaseClass> to Action<DerivedClass> because consuming code is always putting values into these objects – again it is type safe to expect a BaseClass and be given a DerivedClass. If you also try this in previous versions of .NET it will not even compile.

...And that's it.

Having been through a few headaches to understand all this, I do now struggle to think when I would benefit from it; maybe Jeffrey Richter was right after all ;-)

No comments:

Post a Comment