C# 4: Generic variance for delegates

Here are some experiments regarding variance of type parameters for generic delegates in C# 4. There are two kinds of variance: covariance and contravariance. I think of variance as using extended type in place of base type (you could visualise it as a UML diagram, where extended type points to base type). Contravariance is just the opposite: using base type in plase of extended type.

If delgate’s type paramter is variant, it means that we can assign a delegate instance with diffrent type paramter to delegates variable.

Some basic delgates types with variant paramters in C# are Func and Action, simplest of which are:
delegate void Action<in T>(T obj), where in T is contravariant parameter
and
delegate TResult Func<out TResult>(), where out TResult is covariant parameter.

It gets pretty funny when it turns out, that in some cases the default variance type can be inverted when delegate type (with variant type paramter) is nested in another delegate type (i.e. when delegate with variant type paramter becomes a parameter for another delegate). Before showing some cases, let’s bring up a terse rule from a great book “C# in depth” by Jon Skeet: “As a quick rule of thumb, you can think of nested contravariance as reversing the previous variance, whereas covariance doesn’t, so whereas Action<Action> is covariant in T, Action<Action<Action>> is contravariant. Compare that with Func variance, where you can write Func<Func<Func<...Func<T>...>>> with as many levels of nesting as you like and still get covariance”.

For the sake of latter examples, let’s assume that we have a following class hierachy:

Object <- Person <- Child

Let’s start with simple Func covariance:

Func<object> objectOutputFunc = () => new object();
Func<Person> personOutputFunc = () => new Person();
Func<Child> childOuputFunc = () => new Child();
// delegate expected to return Person can return Child
personOutputFunc = childOuputFunc;
personOutputFunc = () => new Child();
// won't compile: personOutputFunc = objectOutputFunc;
// won't compile: personOutputFunc = () => new object();

And some obvious behavior of Action covariance:

// an action, which will be fed with object
Action<object> actionObject = new Action<object>(o => Console.WriteLine(o));
// an action, which will be fed with Person
Action<Person> actionPerson;
// an action, which will be fed with Child
Action<Child> actionChild;
// wont compile (syntax): actionPerson = (Object o) => Console.WriteLine(o);
// an action which expects an object can be fed with a person
actionPerson = actionObject;

//actionPerson(obj);
actionPerson(person);
actionPerson(child);

As noted earlier, nesting Func inside Func does not change type paramter variance (i.e. T in Func<Func<Func<...Func<T>...>>> is always contravariant).

Let’s check what happens when we nest Action inside Func:

Func<Action<object>> funcActionObject = () => o => Console.WriteLine(o);
Func<Action<Person>> funcActionPerson = () => p => Console.WriteLine(p);
Func<Action<Child>> funcActionChild = () => c => Console.WriteLine(c);

// action type parameter is contravariant and so it is here
funcActionPerson = funcActionObject;

Func<Func<Action<object>>> funcFuncActionObject = () => () => o => Console.WriteLine(o);
Func<Func<Action<Person>>> funcFuncActionPerson = () => () => p => Console.WriteLine(p);
Func<Func<Action<Child>>> funcFuncActionChild = () => () => c => Console.WriteLine(c);

// action type parameter is contravariant and so it is here
funcFuncActionPerson = funcFuncActionObject;

Func did not change variance o Action parameter, so Jon Skeet’s rule still applies (that should be no surprise).

Now, let’s see how Action changes variance of nested Func:

Action<Func<object>> actionFuncObject = (f) => f.Invoke();
Action<Func<Person>> actionFuncPerson = (f) => f.Invoke();
Action<Func<Child>> actionFuncChild = (f) => f.Invoke ();

// won't compile: actionFuncPerson = actionFuncChild;

// normally, func is covariant,
// but here, type parameter became contravariant
actionFuncPerson = actionFuncObject;
// won't compile: actionFuncPerson = (Func<object> f) => f.Invoke();
Action<Func<Func<object>>> actionFuncFuncObject = (ffo) => ffo.Invoke().Invoke();
Action<Func<Func<Person>>> actionFuncFuncPerson = (ffp) => ffp.Invoke().Invoke();
Action<Func<Func<Child>>> actionFuncFuncChild = (ffc) => ffc.Invoke().Invoke();

// normally, func is covariant,
// but here, type parameter became contravariant
actionFuncFuncPerson = actionFuncFuncObject;
// won't compile: actionFuncFuncPerson = (Func<Func<object>> ffo) => ffo.Invoke().Invoke();

Indeed, when nesting Func inside Action, variance of type parameter of nested Func changed.

Finally, let’s check how type parameters variance change when nesting sequences of Actions:

// ======== Action<Person> fun ========
// action expects an object to work on.
// actual object to work on is supplied in place of action invocation

// an action, which will be fed with object
Action<object> actionObject = new Action<object>(o => Console.WriteLine(o));
// an action, which will be fed with Person
Action<Person> actionPerson;
// an action, which will be fed with Child
Action<Child> actionChild;

// wont compile (syntax): actionPerson = (Object o) => Console.WriteLine(o);
// an action which expects an object can be fed with a person
actionPerson = actionObject;

//actionPerson(obj);
actionPerson(person);
actionPerson(child);

// ======== Action<Action<Person>> fun ========
// action expects another action which will be suplied with the object by the first action.
// actual object to work on is supplied in place of action definition

// an action, which will be fed with an action for object
			Action<Action<object>> actionActionObject;

// an action, which will be fed with an action for person
Action<Action<Person>> actionActionPerson;

// an action, which will be fed with an action for child
Action<Action<Child>> actionActionChild = new Action<Action<Child>>(a => a.Invoke (child));

// an action, which expects an action to which it may pass a child,
// can be fed with an action which expects a person
actionActionPerson = actionActionChild;
actionActionPerson(new Action<object>(o => Console.WriteLine("Object: " + o))); 
actionActionPerson(p => Console.WriteLine("Person name: " + p.Name));  
//actionActionPerson(new Action<Child>(c => Console.WriteLine("Child toys: " + c.NumberOfToys)));  
actionActionChild(new Action<Child>(c => Console.WriteLine("Child toys: " + c.NumberOfToys)));  

// ======== ActionAction<<Action<Person>>> fun ========
// action (1) expects another action (2), which will be suplied with another action (from action 1) 
// and that another action will work on parameter supplied "within" action (1).
// actual object to work on can be supplied in place of action invocation (simplest case).

Action<Action<Action<object>>> actionActionActionObject = a => a.Invoke(o => Console.WriteLine("Tripple action: " + o));
Action<Action<Action<Person>>> actionActionActionPerson;
Action<Action<Action<Child>>> actionActionActionChild;

actionActionActionObject(ao => ao.Invoke(obj));
actionActionActionPerson = actionActionActionObject;
actionActionActionPerson(ap => ap.Invoke(person));

// ======== ActionActionAction<<Action<Person>>> fun ========
			
Action<Action<Action<Action<object>>>> actionActionActionActionObject;
Action<Action<Action<Action<Person>>>> actionActionActionActionPerson;
Action<Action<Action<Action<Child>>>> actionActionActionActionChild = a => a.Invoke(a2 => a2.Invoke(child));
			
actionActionActionActionChild(a => a.Invoke(c => Console.WriteLine("Quadruple action: Child toys: " + c.NumberOfToys)));
//actionActionActionPerson = actionActionActionActionChild;
//actionActionActionPerson(ap => ap.Invoke(person)); 

Hope you had fun!

Advertisements
  1. Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: