Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to understand contravariance when using generic delegates?

I am learning "contravariant generic delegate".

My understanding is:

The "in" keyword specifies that the type parameter is contravariant.
This allows for implicit conversion of delegate types.

If there is no "in" keyword, we don't know if the type parameter is contravariant.
Then implicit conversion of delegate types are not allowed.

Here is my code:

public class Test
{
    //public delegate bool FuncDelegate<T>(T t);
    public delegate bool FuncDelegate<in T>(T t);

    public class BaseClass
    {
        public int x;
    }

    public class DerivedClass: BaseClass
    {
        public int y;
    }

    static bool BaseFunc(BaseClass bc)
    {
        if (bc.x > 1)
            return false;
        else
            return true;
    }

    static bool DerivedFunc(DerivedClass dc)
    {
        if (dc.y > 1)
            return false;
        else
            return true;
    }

    public static void Main()
    {
        FuncDelegate<DerivedClass> genericDerivedFunc = DerivedFunc;
        FuncDelegate<BaseClass> genericBaseFunc = BaseFunc;

        genericDerivedFunc = genericBaseFunc;

        FuncDelegate<DerivedClass> genericDerivedFunc2 = BaseFunc;
    }
}

My question

/*
    This line is valid when declared as: public delegate bool FuncDelegate<in T>(T t);
    This line is invalid when declared as: public delegate bool FuncDelegate<T>(T t);
*/
genericDerivedFunc = genericBaseFunc;

This line agrees with my undertanding.

/*
    This line is always valid.
*/
FuncDelegate<DerivedClass> genericDerivedFunc2 = BaseFunc;

I don't understand this line:

"bool BaseFunc(BaseClass bc)" can implicitly converts to bool "FuncDelegate<DerivedClass>(DerivedClass t)".

I think it must have the "in" keyword to specifies contravariant.

But the conversion can be done without the "in" keyword.

like image 676
AdmiralOrange Avatar asked Aug 24 '21 08:08

AdmiralOrange


People also ask

What is covariance and Contravariance in generics?

Covariance and contravariance are terms that refer to the ability to use a more derived type (more specific) or a less derived type (less specific) than originally specified. Generic type parameters support covariance and contravariance to provide greater flexibility in assigning and using generic types.

Which delegates support covariance and Contravariance in C#?

In . NET Framework 4 and later versions, C# supports covariance and contravariance in generic interfaces and delegates and allows for implicit conversion of generic type parameters.

What is the difference between covariance and Contravariance?

Covariance permits a method to have a return type that is a subtype of the one defined in the delegate. Contravariance permits a method to have a parameter type that is a base type of the one defined in the delegate type.

How do you define generic delegates?

Delegates defined within a generic class can use the generic class type parameters in the same way that class methods do. Generic delegates are especially useful in defining events based on the typical design pattern because the sender argument can be strongly typed and no longer has to be cast to and from Object.

Can an interface or delegate have both covariant and contravariant type parameters?

An interface or delegate type can have both covariant and contravariant type parameters. Visual Basic and C# do not allow you to violate the rules for using covariant and contravariant type parameters, or to add covariance and contravariance annotations to the type parameters of types other than interfaces and delegates.

Why do generic type parameters support covariance and contravariance?

Generic type parameters support covariance and contravariance to provide greater flexibility in assigning and using generic types. When you are referring to a type system, covariance, contravariance, and invariance have the following definitions.

Does variance in delegate binding work with variant types?

However, variance in delegate binding works with all delegate types, not just with generic delegate types that have variant type parameters.

What is the last generic type parameter of the func generic delegates?

The last generic type parameter of the Func generic delegates specifies the type of the return value in the delegate signature. It is covariant (out keyword), whereas the other generic type parameters are contravariant (in keyword).


2 Answers

Note the difference between the right hand sides of these two assignments:

genericDerivedFunc = genericBaseFunc;
genericDerivedFunc2 = BaseFunc;

The first line's right hand side is a delegate, so you are converting a delegate type to another delegate type. This requires a variance conversion, as listed in the available conversions in the C# spec:

The implicit reference conversions are:

  • ...
  • From any reference_type to an interface or delegate type T if it has an implicit identity or reference conversion to an interface or delegate type T0 and T0 is variance-convertible to T.

And variance conversions require those ins and outs.

On the second line though, the right hand side is a method group (the name of a method), so on the second line, you are actually doing a method group conversion. For such a conversion to be available, BaseFunc needs to be compatible with the target delegate type. Note that this is a requirement about the method, not a requirement on the delegate type. To be "compatible".

Notably, two of the requirements for method M to be "compatible" with a delegate type D are:

  • For each value parameter, an identity conversion or implicit reference conversion exists from the parameter type in D to the corresponding parameter type in M.
  • An identity or implicit reference conversion exists from the return type of M to the return type of D.

These are the requirements that make it look as if the delegate type had the in modifier on all its parameters, and out on its return type.

Basically, because the RHSs are very different kind of things, different rules apply.

like image 197
Sweeper Avatar answered Nov 01 '22 07:11

Sweeper


Docs cover this in variance in delegates section:

.NET Framework 3.5 introduced variance support for matching method signatures with delegate types in all delegates in C#. This means that you can assign to delegates not only methods that have matching signatures, but also methods that return more derived types (covariance) or that accept parameters that have less derived types (contravariance) than that specified by the delegate type. This includes both generic and non-generic delegates.

So in your case this directly falls under the "accept parameters that have less derived types (contravariance) than that specified by the delegate type" part.

If you see the decompilation by sharplab you see that FuncDelegate<DerivedClass> genericDerivedFunc2 = BaseFunc; is actually converted to something like:

FuncDelegate<DerivedClass> genericDerivedFunc2 = new FuncDelegate<DerivedClass>(BaseFunc);

And genericDerivedFunc = genericBaseFunc; is just a plain assignment of FuncDelegate<BaseClass> to FuncDelegate<DerivedClass> which will fail when FuncDelegate is not contravariant.

like image 35
Guru Stron Avatar answered Nov 01 '22 06:11

Guru Stron