Sunday, June 23, 2024 20:46

Table of contents >> Delegates, Lambda Expressions, Events > Delegate covariance and contravariance

Delegate covariance and contravariance

Let’s talk about something that you should not encounter in your day to day programming experience, but nevertheless, you should be aware of, if you want to become a professional engineer: covariance and contravariance with delegates. They are general programming terms, so, you will encounter them in other programming languages as well, not just C#.

To start with, let’s create a base class named Base, and a derived class that inherits it, named Derived:

Next, let’s declare two methods in our Program class, that return Base and Derived types:

Usually, we wouldn’t return null in our methods, but since I am only trying to make a point and we are dealing only with compile time types, null will do fine.

Finally, let’s declare two delegates that will also accept methods that return Base and Derived types and take no arguments:

Now, let’s assign these delegates one at a time. We will start with ReturnsBaseDelegate, and we will assign both methods to it:

At this point, you will notice no errors, your program will compile and run just fine. Let’s analyze what the compiler has to do in this case: first of all, remember that delegates only accept methods that match their signature. In my example, ReturnsBaseDelegate returns a Base, so when I instantiate this delegate and I assign a method that also returns a Base to it, ReturnBase(), there is a perfect match, because both the delegate and the method assigned to it use the Base type. But notice that in the second assignment, I offer my delegate a method that returns Derived type, not Base type! Why isn’t the compiler generating an error? Why are we able to assign ReturnDerived(), which returns a Derived type, to a delegate that accepts a Base type?

If you think about it, every time we invoke ReturnDerived(), the compile time restriction is that it MUST return a Derived type, it can’t return an object, it can’t return a string, it can’t return a Base – it can only return a Derived, or anything that inherits from Derived. So, the compiler knows there is NO way that our ReturnDerived() method can violate the fact that that ReturnsBaseDelegate must return a Base, because a Derived object IS a Base. Since Derived inherits from Base, it HAS all properties and methods and fields and anything else of the Base, so, it IS a Base.

And that is what covariance means: we are allowed to return objects that inherit from the type the delegate return signature expects, even if, obviously, those objects are not the ones declared in the delegate return signature. We can say that the return type between the Derived return method and the Base signature delegate is covariant.

To sum covariance up: when we declare a delegate whose signature states that it returns type a, we are allowed to assign methods that return type a to it, but we are also allowed to assign to it methods that return type b, but only if b is a derived type from a.

In this new light, let’s take our second delegate, ReturnsDerivedDelegate, and do the exact same thing we did with the first delegate:

Now, you will get an error line under the assignment of ReturnBase() method. You can deduce the cause easily, if you think about it: the ReturnsDerivedDelegate delegate says that it will always return something very specific, namely, a Derived. However, ReturnBase() method is not forced to return something as specific as Derived. It can return a Base, or it can return ANY other type that inherits from Base. For this reason, we will get a compiler error. On the other hand, ReturnDerived() method returns a Derived, which is exactly the type that ReturnsDerivedDelegate also returns. No issues there, it is a perfect match.

Let’s talk about contravariance now. I will modify my delegates to accept Derived and Base types as parameters, and return void, and I will also modify my methods to return void and accept a Base and Derived types as parameters:

And now, let’s assign both methods to each delegate, like we did the first time. Let’s start with TakesDerivedDelegate:

In this case, when we invoke our delegate, we must pass a Derived type as parameter, we can’t pass a Base, because Base is more general than Derived. The delegate requires something very specific, a Derived type, and because of that, it satisfies both methods requirements. When we think about it, the TakeDerived() assignment is a perfect match, both the delegate and the method assigned to it use a Derived type. But when I give my delegate the TakeBase() method, since TakeBase() takes a Base, that’s more general than what TakesDerivedDelegate will require. When I invoke TakesDerivedDelegate, I can only pass a Derived as the invocation parameter, or anything that inherits from Derived. This means there is NO way I will ever pass anything more general than a Derived, and since Derived IS a Base, I can pass it to the TakeBase() method, because that method accepts types more general than Derived. It can take a Derived, but it can take other things, like Base or anything that inherits from Base too. In other words, we can point our delegate to a very relaxed method because the delegate is very strict, and will never assign things that the method will not accept.

To end this lesson, let’s take TakesBaseDelegate delegate too, and assign both methods to it:

We will now get a compiler error under our TakeDerived() method assignment, just as we did in the case of covariance. Obviously, if the delegate accepts a Base as a parameter, and we point it to a method that also takes a Base as a parameter, there is a perfect match and everything works fine. On the other hand, when I try to assign TakeDerived() to a delegate that accepts a Base, that’s not possible, because the delegate expects a more general type, and we are offering it something very specific. A Derived can only be a Derived, while a Base can be anything that inherits from it, including Derived. In this case, we can say that the argument Base is not contravariant with Derived.

Tags: , , ,

4 Responses to “Delegate covariance and contravariance”

  1. Ram says:

    This is honestly a great resource for those looking to learn programming and trying to understand the fundamentals. Based some of the posts and the video on Casting and type conversion; just wanted to thank you for taking the time to do this for people. Really inspiring to see someone put this much time aside to help others understand. Heart felt thank you from Canada. 🙂

    • rusoaica says:

      Thank you very much! I’m happy if it helped you!
      It is my personal opinion that everyone should contribute something for the greater good, that everything would be better if everyone did so.. Just trying to do my part.

  2. ashwani says:

    there is lot of simplicity and clarity while explaining advance and complex concepts. u have simultaneously explained theory and practicals, thank u very much

Leave a Reply

Follow the white rabbit