Understanding C# Covariance and Contravariance (1) Delegates

Understanding C# Covariance And Conreavariance:

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.

2 Comments

Comments have been disabled for this content.