Covariance and Contravariance in C#, Part Four: Real Delegate Variance

Covariance and Contravariance in C#, Part Four: Real Delegate Variance

Rate This
  • Comments 8

In the last two posts I discussed the two kinds of variance that C# already has -- array covariance and member-group-to-delegate conversion covariance (on return types) and contravariance (on formal parameter types).

Today I want to generalize the latter kind of variance.

In C# 3.0 today, even though it is legal to assign a typeless method group for a function that returns a Giraffe to a variable of type Func<Animal>, it is not legal to assign a typed expression of type Func<Giraffe> to a Func<Animal>. Generic delegate types are always invariant in C# 3.0. That seems weak.

Suppose we had the ability to declare type parameters of generic delegate types as being covariant or contravariant. For the sake of brevity (and consistency with existing notation in the CLR specification) I will notate a covariant type parameter with a + and a contravariant type parameter with a -.

This is not a particularly compelling notation; I will discuss its deficiencies in a later post. But for now we'll stick with it.The way to remember what it means is that a plus means "this type argument is allowed to get bigger upon assignment", and similarly for minus.

Consider for example our standard function:

delegate R Func<A, R>(A a);

Since R appears only in the returns and A appears only in the formal parameter list, we can make R covariant and A contravariant(‡):

delegate R Func< -A, +R >(A a);

So again, you can think of this as "you can make A smaller or R bigger" (or, of course, both). For example:

Func<Animal, Giraffe> f1 = whatever;
Func<Mammal, Mammal> f2 = f1;

Normally in C# this assignment would be illegal because the delegates are parameterized by different types. But since we have made Func variant in both its type parameters, this assignment would become legal were we to add this kind of variance to a hypothetical future version of C#.

Does that make sense so far?

(‡) This rule of thumb is not always correct! Sometimes the input parameters need to be of a covariant type parameter. I shall discuss just such a situation next time, and I promise that it will hurt your brain.

  • Maybe sleep deprivation is negatively impacting my comprehension, but shouldn't your example be...

    Func<Giraffe, Animal> f1 = whatever;

    Func<Mammal, Mammal> f2 = f1;

    ...since the argument type parameter A can be substituted with a 'smaller' type, and the return type parameter R can be substituted with a 'bigger' type?

  • er, nevermind.  I get it now.  You'd be passing in a Mammal to something that expects an Animal, and you'd get a Giraffe returned when expecting a Mammal, both of which are perfectly legal.  I guess I'm just having a bad brain day.

  • Like I said, this stuff has a way of hurting the brain. Just wait until next time, it'll get even worse!

  • That makes perfect sense. I came across this problem when I implemented my favorite "generic interface/base class" pattern for an expression parser:

    public interface IOperation : IExpression

    {

     Collection<IExpression> Operands { get; }

     object EvaluateOperand(IExpression operand);

    }

    public interface IOperation<TResult, TOperand> : IExpression<TResult>, IOperation

    {

     new Collection<IExpression<TOperand>> Operands { get; }

     new TOperand EvaluateOperand(IExpression<TOperand> operand);

    }

    public abstract class Operation<TResult, TOperand> : Expression<TResult>, IOperation<TResult, TOperand>

    {

     // Implement members using the type parameters

     public Collection<IExpression<TOperand>> Operands

     {

       get { ... }

     }

     public object EvaluateOperand(IExpression<TOperand> operand)

     {

       ...

     }

     // Explicitly implement the untyped interface using the typed members

     Collection<IExpression> IOperation.Operands

     {

       get { return this.Operands; }  // ERROR - Invalid cast due to lack of variance

     }

     object IOperation.EvaluateOperand(IExpression operand)

     {

       return EvaluateOperand((IExpression<TOperand>) operand);

     }

    }

  • A handy workaround, available today, to the limitation in delegate variance compatibility is to assign the Invoke method instead. (Compilers invoking a delegate are actually calling the Invoke method under the covers.) So:

    Func<Animal, Giraffe> f1 = whatever;

    Func<Mammal, Mammal> f2 = f1.Invoke; // ok

    It does result in chaining rather than conversion, but the semantic is the same.

    Covariant input argument in the context of virtual methods in an object-oriented language makes perfect sense when it's the 'this' parameter (it can't be any other way, really), and C++ also supports covariance of the return type for just such scenarios, unlike the CLR.

    I'm interested to find out what kind of variance you'll present!

  • This sounds nice. We currently work around things with:

    delegate T GenProvider<T>();

           /// <summary>

           /// converts the delegate to the appropriate genric provider

           /// </summary>

           public static GenProvider<T> MakeProvider<T,S>(GenProvider<S> s) where S : T

           {

               return (GenProvider<T>)Delegate.CreateDelegate(typeof(GenProvider<T>), s.Target, s.Method);

           }

    What would be really nice is general structural typing rather than named typing for delegates.

    e.g.

    delegate void F();

    delegate void G();

    F f  = delegate () { };

    G g = f;

    When Func<T> comes along there will be quite a few classes where we would have to go through and change all the existing delegate types to be Func<T> which will be tedious and binary breaking despite the method call's code path's being fully compatible under the hood.

    I appreciate why you might not want that (though some syntactic sugar to 'cast' it away would be nice rather than having to magic up a new delegate as above)

  • So nicely step by step blogged by Eric Lippert for &quot;Covariance and Contravariance&quot; as &quot;Fabulous

  • As what I understand, covariance is CASTING a more spcecific type to a more general one, such as Dog to Animal. Contravariance is the opposite, such as Animal to Dog.

    In the example 'delegate R Func< -A, +R >(A a)', the reason that we can make R bigger is that R is passed from the method back to the delegate, so it is acceptable if you pass a more specific type to a more general type. While the reason you can make A samller is that A is passed from the delegate to the method, so I think it is still casting a more specific type to a more general type, it is still covariance not contravariance.

    Please let me know if I am wrong. Thanks!

Page 1 of 1 (8 items)