Understanding C# Covariance and Contravariance (1) Delegates
Understanding C# Covariance And Conreavariance:
- Understanding C# Covariance And Contravariance (1) Delegates
- Understanding C# Covariance And Contravariance (2) Interfaces
- Understanding C# Covariance And Contravariance (3) Samples
- Understanding C# Covariance And Contravariance (4) Arrays
- Understanding C# Covariance And Contravariance (5) Higher-order Functions
- Understanding C# Covariance And Contravariance (6) Typing Issues
- Understanding C# Covariance And Contravariance (7) CLR
- Understanding C# Covariance And Contravariance (8) Struct And Void
In Covariance/contravariance, variance is the capability to replace a type with a less-derived type or a more-derived type in a context. C# 4.0 and CLR 4 introduced covariance and contravariance for generics.
Is-a relationship for inheritance
Since covariance and contravariance is about deriving, the following inheritance hierarchy is defined:
public class Base { } public class Derived : Base { }
Apparently, a Derived object “is a” Base object.
Non-generic delegate
By using above Base/Derived as input/output of method, there are 4 combinations:
public static class Methods { public static Base DerivedIn_BaseOut(Derived @in) { return new Base(); } public static Derived DerivedIn_DerivedOut(Derived @in) { return new Derived(); } public static Base BaseIn_BaseOut(Base @in) { return new Base(); } public static Derived BaseIn_DerivedOut(Base @in) { return new Derived(); } }
Bind method to a delegate
Before C# 4.0, C# already supported covariance and contravariance for delegates without generics. Consider the following delegate type:
public delegate Base DerivedIn_BaseOut(Derived @in);
Above Methods.DerivedIn_BaseOut’s signature matches this delegate type, so Methods.DerivedIn_BaseOut can be bound to its delegate instance:
public static partial class NonGenericDelegate { public static void Bind() { // Binding: DerivedIn_BaseOut delegate type and DerivedIn_BaseOut method have exactly the same signature. DerivedIn_BaseOut derivedIn_BaseOut = Methods.DerivedIn_BaseOut; // When calling derivedIn_BaseOut delegate instance, DerivedIn_BaseOut method executes. Base @out = derivedIn_BaseOut(@in: new Derived()); } }
Covariance
Methods.DerivedIn_DerivedOut has a different signature from DerivedIn_BaseOut delegate type. The former returns a more derived type. There is a “is-a” relationship between their return types, but there is no intuitive relationship between the two signatures.
However, C# compiler and the CLR both allow the following binding (assignment) before C# 4.0:
public static partial class NonGenericDelegate { public static void Covariance() { // Covariance: Derived "is a" Base => DerivedIn_DerivedOut "is a" DerivedIn_BaseOut. DerivedIn_BaseOut derivedIn_DerivedOut = Methods.DerivedIn_DerivedOut; // When calling derivedIn_BaseOut delegate instance, DerivedIn_DerivedOut method executes. // derivedIn_BaseOut should output a Base object, while DerivedIn_DerivedOut outputs a Derived object. // The actual Derived object "is a" required Base output. This binding always works. Base @out = derivedIn_DerivedOut(@in: new Derived()); } }
Here a bound method can return a more derived type than the delegate type. This is called covariance.
Contravariance
Methods.BaseIn_BaseOut required a less-derived parameter then DerivedIn_BaseOut delegate type. The following binding also works before C# 4.0:
public static partial class NonGenericDelegate { public static void Contravariance() { // Contravariance: Derived is a Base => BaseIn_BaseOut is a DerivedIn_BaseOut. DerivedIn_BaseOut derivedIn_BaseOut = Methods.BaseIn_BaseOut; // When calling derivedIn_BaseOut delegate instance, BaseIn_BaseOut method executes. // derivedIn_BaseOut should have a Derived input, while BaseIn_BaseOut requires a Base input. // The actual Derived object "is a" required Base input. This binding always works. Base @out = derivedIn_BaseOut(@in: new Derived()); } }
Here a method can have less derived parameter type than the delegate type. This is called contravariance.
Covariance and contravariance
It is easy to predict, Methods.BaseIn_DerivedOut, with more derived parameter type and less derived return type, can be also bound to DerivedIn_BaseOut:
public static partial class NonGenericDelegate { public static void CovarianceAndContravariance() { // Covariance and contravariance: Derived is a Base => BaseIn_DerivedOut is a DerivedIn_BaseOut. DerivedIn_BaseOut derivedIn_BaseOut = Methods.BaseIn_DerivedOut; // When calling derivedInBaseOut delegate instance, BaseIn_DerivedOut method executes. // derivedIn_BaseOut should have a Derived input, while BaseIn_DerivedOut requires a Base input. // derivedIn_BaseOut should output a Base object, while BaseIn_DerivedOut outputs a Derived object. // This binding always works. Base @out = derivedIn_BaseOut(@in: new Derived()); } }
Here covariance and contravariance both happen for the same binding.
Invalid variance
In the following bindings, there is no valid variance, so they cannot be compiled:
public static partial class NonGenericDelegate { public delegate Derived BaseIn_DerivedOut(Base @base); public static void InvalidVariance() { #if Uncompilable // baseIn_DerivedOut should output a Derived object, while BaseIn_DerivedOut outputs a Base object. // Base is not Derived, the following binding cannot be compiled. BaseIn_DerivedOut baseIn_DerivedOut1 = Methods.BaseIn_BaseOut; // baseIn_DerivedOut should have a Base input, while DerivedIn_BaseOut required a Derived output. // Base is not a Derived, the following binding cannot be compiled. BaseIn_DerivedOut baseIn_DerivedOut2 = Methods.DerivedIn_BaseOut; // baseIn_DerivedOut should have a Base input, while DerivedIn_DerivedOut required a Derived input. // baseIn_DerivedOut should output a Derived object, while derivedIn_DerivedOut outputs a Base object. // Base is not a Derived, the following binding cannot be compiled. BaseIn_DerivedOut baseIn_DerivedOut3 = Methods.DerivedIn_DerivedOut; #endif } }
Is-a relationship of delegates
The root of variances is that, in inheritance hierarchy, derived object “is a” base object. This “is-a” relationship can be promoted to a relationship between method and delegate types:
- Covariance of output: Derived is a Base => DerivedIn_DerivedOut is a DerivedIn_BaseOut;
- Contravariance of input: Derived is a Base => BaseIn_BaseOut is a DerivedIn_BaseOut;
- Covariance of output and contravariance of input: Derived is a Base => BaseIn_DerivedOut is a DerivedIn_BaseOut.
Please notice these rules does not apply to value types. Basically value types has nothing to do with covariance/contravariance.
Generic delegate
With C# 2.0 generic delegate, the above XxxIn_XxxOut delegate types can be represented by the following:
public delegate TOut Func<TIn, TOut>(TIn @in);
Then above method bindings become:
public static partial class GenericDelegateWithVariances { public static void BindMethods() { // Bind. Func<Derived, Base> derivedIn_BaseOut1 = Methods.DerivedIn_BaseOut; // Covariance. Func<Derived, Base> derivedIn_BaseOut2 = Methods.DerivedIn_DerivedOut; // Contravariance. Func<Derived, Base> derivedIn_BaseOut3 = Methods.BaseIn_BaseOut; // Covariance and contravariance. Func<Derived, Base> derivedIn_BaseOut4 = Methods.BaseIn_DerivedOut; } }
C# 3.0 introduced lambda expression. However, the above bindings cannot be used for lambda expression:
public static partial class GenericDelegate { public static void BindLambdas() { Func<Derived, Base> derivedIn_BaseOut = (Derived @in) => new Base(); Func<Derived, Derived> derivedIn_DerivedOut = (Derived @in) => new Derived(); Func<Base, Base> baseIn_BaseOut = (Base @in) => new Base(); Func<Base, Derived> baseIn_DerivedOut = (Base @in) => new Derived(); #if Uncompilable // Covariance. derivedIn_BaseOut = derivedIn_DerivedOut; // Contravariance. derivedIn_BaseOut = baseIn_BaseOut; // Covariance and contravariance. derivedIn_BaseOut = baseIn_DerivedOut; #endif } }
The out and in keywords
C# 4.0 uses the in/out keywords to specify a type parameter is contravariant/covariant. So above generic delegate can be defined as:
public delegate TOut Func<in TIn, out TOut>(TIn @in);
Now the bindings work for both methods and lambda expressions:
public static partial class GenericDelegateWithVariances { public static void BindMethods() { // Bind. Func<Derived, Base> derivedIn_BaseOut1 = Methods.DerivedIn_BaseOut; // Covariance. Func<Derived, Base> derivedIn_BaseOut2 = Methods.DerivedIn_DerivedOut; // Contravariance. Func<Derived, Base> derivedIn_BaseOut3 = Methods.BaseIn_BaseOut; // Covariance and contravariance. Func<Derived, Base> derivedIn_BaseOut4 = Methods.BaseIn_DerivedOut; } public static void BindLambdas() { Func<Derived, Base> derivedIn_BaseOut = (Derived @in) => new Base(); Func<Derived, Derived> derivedIn_DerivedOut = (Derived @in) => new Derived(); Func<Base, Base> baseIn_BaseOut = (Base @in) => new Base(); Func<Base, Derived> baseIn_DerivedOut = (Base @in) => new Derived(); // Covariance. derivedIn_BaseOut = derivedIn_DerivedOut; // Contravariance. derivedIn_BaseOut = baseIn_BaseOut; // Covariance and ontravariance. derivedIn_BaseOut = baseIn_DerivedOut; } }
The in/out keywords also constrains the usage of the decorated type parameter to guarantee the variances. The following generic delegate types are invalid and cannot be compiled:
public static partial class GenericDelegateWithVariances { #if Uncompilable // CS1961 Invalid variance: The type parameter 'TOut' must be covariantly valid on 'GenericDelegateWithVariances.Func<TOut>.Invoke()'. 'TOut' is contravariant. public delegate TOut Func<in TOut>(); // CS1961 Invalid variance: The type parameter 'TIn' must be contravariantly valid on 'GenericDelegateWithVariances.Action<TIn>.Invoke(TIn)'. 'TIn' is covariant. public delegate void Action<out TIn>(TIn @in); // CS1961 Invalid variance: The type parameter 'TOut' must be covariantly valid on 'GenericDelegateWithVariances.Func<TIn, TOut>.Invoke(TIn)'. 'TOut' is contravariant. // CS1961 Invalid variance: The type parameter 'TIn' must be contravariantly valid on 'GenericDelegateWithVariances.Func<TIn, TOut>.Invoke(TIn)'. 'TIn' is covariant. public delegate TOut Func<out TIn, in TOut>(TIn @in); #endif }
So far, it looks in is only for input, and out is only for output. In .NET 4.0+:
namespace System { public delegate TOut Func<out TOut>(); public delegate TOut Func<out TOut, in TIn>(TIn @in); public delegate TOut Func<out TOut, in TIn1, in TIn2>(TIn1 in1, TIn2 in2); public delegate TOut Func<out TOut, in TIn1, in TIn2, in TIn3>(TIn1 in1, TIn2 in2, TIn3 in3); // ... public delegate void Action<in TIn>(TIn @in); public delegate void Action<in TIn1, in TIn2>(TIn1 in1, TIn2 in2); public delegate void Action<in TIn1, in TIn2, in TIn3>(TIn1 in1, TIn2 in2, TIn3 in3); // ... }
The type parameter is renamed to be more intuitive.