Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does C# use contravariance (not covariance) in input parameters with delegate?

When we have a Base class that inherits BBase and a Derived class that specializes it, let's say there is a delegate that requires Base as an input.

using System;

class BBase {}
class Base : BBase {}
class Derived : Base {}

delegate void BaseDelegate(Base b);

In the usage of the delegate, it is not allowed to use BaseDelegate b2 = TakeDerived; as the input is contravariant.

class MainClass
{
    static void TakeBBase(BBase bb) {}
    static void TakeBase(Base b) {}
    static void TakeDerived(Derived d) {}

    static void Main(string[] args)
    {
        BaseDelegate b1 = TakeBase;
        b1(new Derived());
        b1(new Base());

        // ERROR
        // parameters do not match delegate 
        // `BaseDelegate(Base)' parameters
        // The contract of b2 is to expect only Base
        //BaseDelegate b2 = TakeDerived;

TakeBBase can be assigned to BaseDelegate.

    BaseDelegate b2 = TakeBBase;
    b2(new Derived());
    b2(new Base());

It's also interesting to see that we can assign subclasses of Base class to the Base type parameter in the delegate. covariance/contravariance rule does not seem to work in the previous example.

  • Why does C# choose to use contravariance (not covariance) in input parameters in delegate?
  • When covariance/contravariance rule is working in C#? What other cases than delegate use covariance/contravariance and why?
like image 288
prosseek Avatar asked May 26 '16 17:05

prosseek


People also ask

Why is hashtag used in C?

Application: The ## provides a way to concatenate actual arguments during macro expansion. If a parameter in the replacement text is adjacent to a ##, the parameter is replaced by the actual argument, the ## and surrounding white space are removed, and the result is re-scanned.

What is '#' in C language?

'#' is called pre-processor directive and the word after '#' is called pre-processor command. Pre-processor is a program which performs before compilation. Each pre-processing directive must be on its own line.

Why is C so tough?

It is hard to learn because: It has complex syntax to support versatility. It is a permissive language—you can do everything that's technically possible, even if not logically right. It is best learned by someone who already has a foundation with C programming.

Why do we use symbol in C?

The most common use of symbols by programmers is for performing language reflection (particularly for callbacks), and most common indirectly is their use to create object linkages. In the most trivial implementation, they are essentially named integers (e.g. the enumerated type in C).


1 Answers

Olivier's answer is correct; I thought I might try to explain this more intuitively.

Why does C# choose to use contravariance (not covariance) in input parameters in delegate?

Because contravariance is typesafe, covariance is not.

Instead of Base, let's say Mammal:

delegate void MammalDelegate(Mammal m);

This means "a function that takes a mammal and returns nothing".

So, suppose we have

void M(Giraffe x)

Can we use that as a mammal delegate? No. A mammal delegate must be able to accept any mammal, but M does not accept cats, it only accepts giraffes.

void N(Animal x)

Can we use that as a mammal delegate? Yes. A mammal delegate must be able to accept any mammal, and N does accept any mammal.

covariance/contravariance rule does not seem to work in this example.

There is no variance here to begin with. You are making the extremely common mistake of confusing assignment compatibility with covariance. Assignment compatibility is not covariance. Covariance is the property that a type system transformation preserves assignment compatibility.

Let me say that again.

You have a method that takes a Mammal. You can pass it a Giraffe. That is not covariance. That is assignment compatibility. The method has a formal parameter of type Mammal. That is a variable. You have a value of type Giraffe. That value can be assigned to that variable, so it is assignment compatible.

What then is variance, if it is not assignment compatibility? Let's look at an example or two:

A giraffe is assignment compatible with a variable of type mammal. Therefore a sequence of giraffes (IEnumerable<Giraffe>) is assignment compatible with a variable of type sequence of mammals (IEnumerable<Mammal>).

That is covariance. Covariance is the fact that we can deduce the assignment compatibility of two types from the assignment compatibility of two other types. We know that a giraffe may be assigned to a variable of type animal; that lets us deduce another assignment compatibility fact about two other types.

Your delegate example:

A mammal is assignment compatible with a variable of type animal. Therefore a method which takes an animal is assignment compatible with a variable of type delegate which takes a mammal.

That is contravariance. Contravariance is again, the fact that we can deduce the assignment compatibility of two things -- in this case a method can be assigned to a variable of a particular type -- from the assignment compatibility of two other types.

The difference between covariance and contravariance is simply that the "direction" is swapped. With covariance we know that A can be used as B implies that I<A> can be used as I<B>. With contravariance we know that I<B> can be used as I<A>.

Again: variance is a fact about the preservation of an assignment compatibility relationship across a transformation of types. It is not the fact that an instance of a subtype may be assigned to a variable of its supertype.

What other cases than delegate use covariance/contravariance and why?

  • Conversion of method groups to delegates uses covariance and contravariance on return and parameter types. This only works when the return / parameter types are reference types.

  • Generic delegates and interfaces may be marked as covariant or contravariant in their type parameters; the compiler will verify that the variance is always typesafe, and if not, will disallow the variance annotation. This only works when the type arguments are reference types.

  • Arrays where the element type is a reference type are covariant; this is not typesafe but it is legal. That is, you may use a Giraffe[] anywhere that an Animal[] is expected, even though you can put a turtle into an array of animals but not into an array of giraffes. Try to avoid doing that.

Note that C# does NOT support virtual function return type covariance. That is, you may not make a base class method virtual Animal M() and then in a derived class override Giraffe M(). C++ allows this, but C# does not.

UPDATE regarding the previous paragraph: This answer was written in 2016; in 2020, C# 9 does now support return type covariance.

like image 155
Eric Lippert Avatar answered Nov 05 '22 08:11

Eric Lippert