114. Variant generic delegates#
Variation in programming offers the flexibility to adapt code efficiently. With generic delegates, variance takes on a new dimension. In C#, variance for generic delegates means that we can use a more derived type (or a less derived type) than originally specified.
Generic delegates support variance, if we explicitly specify whether a given parameter is covariant or contravariant.
Key points
C# supports variance in generic delegates.
Covariance allows a method with a return type derived from the delegate’s return type and is denoted using the
out
keyword.Contravariance allows a method with a parameter type that’s a base of the delegate’s parameter type and is denoted using the
in
keyword.
Covariant Generic Delegates#
Let’s first explore covariance in generic delegates. Consider the following simple class hierarchy:
class Fruit {}
class Apple : Fruit {}
Then suppose we have a generic delegate that is defined to return a T
:
delegate T Factory<T>();
And that we have a delegate variable of type Factory<Apple
:
// Creating a delegate-instance with a Lambda-expression
// and storing it in a delegate variable of type Factory<Apple>.
Factory<Apple> appleFactory = () => new Apple();
Now, without explicitly stating that T
in Factory<T>
is covariant using the out
keyword, the following attempt to assign a Factory<Apple>
to a Factory<Fruit>
variable does not compile:
Factory<Apple> appleFactory = () => new Apple();
// Trying to use a Factory<Apple> where a Factory<Fruit> is expected.
Factory<Fruit> fruitFactory = appleFactory;
(4,31): error CS0029: Cannot implicitly convert type 'Factory<Apple>' to 'Factory<Fruit>'
However, if we were to explicitly state that T
is covariant by using the out
keyword, then it will compile.
delegate T Factory<out T>();
Factory<Apple> appleFactory = () => new Apple();
// Trying to use a Factory<Apple> where a Factory<Fruit> is expected.
Factory<Fruit> fruitFactory = appleFactory;
Hooray. 🙌
Contravariant Generic Delegates#
Now, let’s explore contravariance in generic delegates using our Fruit
and Apple
classes.
First, let’s define a generic delegate called Consumer<T>
that takes a parameter of type T
:
delegate void Consumer<in T>(T item);
Then let’s define a delegate variable of type Consumer<Fruit>
:
// Creating a delegate-instance with a Lambda-expression
// and storing it in a delegate variable of type Consumer<Fruit>.
Consumer<Fruit> fruitConsumer = (Fruit f) => Console.WriteLine("Nom nom...");
Without the explicit declaration that T
in Consumer<T>
is contravariant using the in
keyword, the following attempt to assign a Consumer<Fruit>
to a Consumer<Apple>
variable won’t compile:
// Trying to use a Consumer<Fruit> where a Consumer<Apple> is expected.
Consumer<Apple> appleConsumer = fruitConsumer;
However, once we explicitly declare T
as contravariant using the in
keyword it works like a charm.
delegate void Consumer<in T>(T item);
Consumer<Fruit> fruitConsumer = (Fruit f) => Console.WriteLine("Nom nom...");
// Using a Consumer<Fruit> where a Consumer<Apple> is expected.
Consumer<Apple> appleConsumer = fruitConsumer;
Hooray again. 🙌
Example#
What’s an example when we might make use of covariant or contravariant generic delegates you ask? Well, the built-in generic delegates Func
, Action
, and Predicate
all have variant parameters.
Consider the delegate Func<T, TResult>
for example. It is approximately defined like below:
delegate TResult Func<in T,out TResult>(T arg);
This means that Func<T, TResult>
is contravariant in T
and covariant in TResult
.
Which, in turn, allows us to compile and run the following code:
public class Fruit
{
public bool IsRipe { get; set; }
}
public class Apple : Fruit { }
// A list of Apples.
List<Apple> apples = new List<Apple>
{
new Apple { IsRipe = true },
new Apple { IsRipe = false }
};
// A method that checks if a Fruit is ripe
Func<Fruit, bool> IsRipeFruit = fruit => fruit.IsRipe;
// Contravariance allows us to use IsRipeFruit.
IEnumerable<Fruit> ripeApples = apples.Where(IsRipeFruit);
In the code above, we’re passing the delegate variable IsRipeFruit
of type Funct<Fruit, bool>
to the LINQ method Where
even though the type Func<Apple, bool>
was expected. This works since delegates are contravariant in input.
Conclusion#
Having the ability to assign methods with more derived (or less derived) types to generic delegates provides flexibility. It means our methods can be more general-purpose, yet still be used in specific scenarios. This results in code that’s not only reusable and adaptable but also still statically type-safe.