How to understand contravariance when using generic delegates?
Asked Answered
M

2

5

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.

Moonstruck answered 24/8, 2021 at 8:25 Comment(1)
not really answers my question.Moonstruck
P
5

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.

Polyphyletic answered 24/8, 2021 at 8:48 Comment(0)
H
2

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.

Halftruth answered 24/8, 2021 at 8:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.