C# variance in delegates

Alright! I had a handy experience in a project (of which I will speak later) which I have decided to share it with you guys and it is about C# Variance in delegates.

When we talk about variances in delegates, we have to consider three aspects of it:

  • Variances between the types defined by the delegate and the types of the function assigned to the delegate
  • Variances between the delegate type and other types (in this case object ).
  • The combinations between those two points.

Delegates are types by reference as if they were a class. That means that “null” is an accepted value and there is no boxing/unboxing when converting to/from an object. Like all types, a delegate inherits from the object :

public delegate void MyDelegate(int i, object o);
object o = new MyDelegate((i, o) => { });

That compiles without problems: we create a MyDelegate linked to an anonymous function (created with a lambda expression) and we assign the value to a variable of type object.

Check the following code:

MyDelegate d = new MyDelegate((i, o) => { });

It is obvious that it works, but we could try to simplify it, eliminating the explicit creation of the MyDelegate object since the lambda expression already defines a valid function:

MyDelegate d = (i, o) => { };

That is also correct. In short, either by explicitly creating the delegated object (first case) or not (second case) we can assign a lambda to a delegate, as long as the parameters of the lambda are “compatible” with the signature of the delegate. For that, of course, the compiler must “infer” the types of lambda parameters. In this case, it assumes that the parameter i is of type int and the parameter or is of type object since that is the way to comply with the signature of the delegate.

Okay, then we can assign delegates to objects and we can create delegates from lambdas and the compiler infers the types of the parameters. But what happens if we try to assign a delegate to a function that does not correspond to the delegate’s signature, but where there is a variance between its parameters?

That is, look at the following snippet of code:

MyDelegate d = (int i, string s) => { };

Here we are forcing the s parameter of the lambda expression to be a string. Is it right? Let’s see:

  • string inherits from object
  • Therefore any function that has as parameters (int, object) we can pass (int, string) without any problem.
    so in a delegate (int, object), we can save a function (int, string).
  • It seems that this should compile, but the reality is that no . This code does not compile. You will receive an error CS1661: Can not convert lambda expression to delegate type ‘MyDelegate’ because the parameter types do not match the delegate parameter types.

The problem is that we have assumed that since we can pass any function (int, object) (int, string), the functions (int, string) are “as a subtype” of the functions (int, object). Or in a more technical words: we have assumed that, since there is conversion between the string and object types (we can use strings where we expect objects) there will be conversion between the types “function (int, object) and” function (int, string) ” And that is not the case.

There is no way to assign a function (int, string) to a delegate whose signature is (int, object) . That’s right! Let’s see it with an example:

That does not work :

public delegate void MyStringDelegate(int i, string s);
public delegate void MyObjectDelegate(int i, object o);

MyStringDelegate d = (int i, string s) => { }; 
MyObjectDelegate t = d;

The last line gives an error, saying that you can not convert a delegate (int, string) to a delegate (int, object). It does not matter that the conversion between string and object. Here does not apply

You may be tempted to use in to force conversion :

public delegate void MyDelegate<in  T>(int i, T o);
MyDelegate<string> d = (int i, string s) => { };
MyDelegate<object> t = d;        // Error CS0266

That is not going to compile either.

And why can not it? Well, very simple. Imagine that any of those two codes was possible. Then I could do:

MyDelegate<string> d = (int i, string s) => { };
MyDelegate<object> t = d;
t(1, DateTime.Now);

I called a function that accepted (int, string) and passed it (int, DateTime)! (In fact, I have passed (int, object), so in practice, I can pass (int, anything )). It is obvious that this should not compile!

Of course, the problem is that a delegate allows me to invoke the function it contains . So, what really looks like a conversion issue (using strings instead of objects) is really a contravariance problem (using objects instead of strings). Therefore, even though a variable of type object can contain a string, a delegate whose signature is (object) can not contain a function with signature (string).

That is, yes, to any function that accepts (int, object) we can pass (int, string) but that is NOT assigning a function (int, string) to a delegate (int, object) !!!

In fact, the use we have tried with the keyword in enables us precisely the opposite conversion :

MyDelegate<object> d = (int i, object s) => { };
MyDelegate<string> t = d;
t(1, "");

See how we can assign a MyDelegate <object> to a MyDelegate <string> . That may sound counter-intuitive (ultimately a variable string can not contain an object), but again it has all the logic if you look at it from the point of view of the delegate: The delegate d contains a function that accepts (int , object), therefore this function will accept parameters (int, string) which is precisely what delegate t requires. All in order!

In this case, we say that the delegate MyDelegate <T> is contravariant with respect to T , which is a way of saying that given a MyDelegate <T> can be assigned to a MyDelegate <U> where U is a subtype of T. The keyword in enables the contravariance.

Instead of contravariance, we can declare that a delegate is covariant with respect to a type. That is, given MyDelegate <T> we can assign it to a MyDelegate <U> where U is a “supertype” of T. For this we use out instead of in when defining the delegate:

public delegate void MyDelegate<out  T>(int i, T o);    // Error CS1961

Of course, that does not compile (if it did we would return to the initial point of passing DateTimes to functions that wait for chains). In fact, for this to work, the generic parameter T must be the return type of the delegate. Take p. ex. Func <T, R>. This delegate is declared as follows:

public delegate R Func<in T, out R>(R r)

That is, Func is contravariant with respect to T and covariant with respect to R. Which means that I can assign a Func <T, R> to any Func <T ‘, R’> whenever:

  • T ‘is a subtype of T
  • R ‘is a supertype of R
    So the following code is correct:
class A { }
class B : A { }
class C : B { }

Func<B, A> f = _ => new A();
Func<C, object> f2 = f;

Observe how the two rules are met:

  • C is a subtype of B (T ‘is a subtype of T)
  • the object is a supertype of A (R ‘is a supertype of R)

That is logical since:

  • Forcing T ‘to be a subtype of T, we avoid passing as a parameter an object that is not allowed: all parameters (of type T’) will be of a type that will be a subtype of the original.
  • Forcing R ‘to be a supertype of R, we avoid that the result is in a wrong reference. In our case, the “real” result (type A) will be stored in a reference of type object (supertype of A), if we invoke the function through f2.

 

You may also like...

Leave a Reply

Your email address will not be published.