ckv
ckv

Reputation: 10830

Covariance/Invariance/Contravariance in delegates in C#

I Have the following piece of code. I have not specified any generic parameters and IN/OUT(variance) for this delegate. If I understand the meaning of invariance correctly i should not be able to return object of Base type since my delegate mentions return type of object.

Is my understanding of invariance wrong?

class Program
{
    public delegate object SampleDelegate(Base b);

    static void Main(string[] args)
    {
        List<Base> listBases = new List<Base>(){new Base{}, new Base{}};
        SampleDelegate newDel = new SampleDelegate(ProcessBase);
        newDel(new Base() { });
        Console.ReadLine();
    }

    public static Base ProcessBase(Base b)
    {
        return b;
    }

    public class Base
    {

    }

    public class Derived : Base
    {
    }   
}

Upvotes: 1

Views: 597

Answers (2)

Jon Hanna
Jon Hanna

Reputation: 113322

All delegates allow a degree of covariance in that one can assign a method with a more derived return type to a delegate. This is what you did here, where you had a method with return type Base and a delegate of return type object.

Essentially the value returned is cast to object on invocation, which will always work, because the cast from Base to object is always going to work.

Or to look at it another way, to call ProcessBase via a delegate of type SampleDelegate is to call (object)ProcessBase(theArgument).

You can think of this as the opposite to how we can always call a method with arguments of more derived types. E.g. we can do ReferenceEquals("abc", 1) because "abc" can be cast to object as a more derived type, and 1 can be cast to òbject` by boxing.

And indeed, for similar reasons you could assign ProcessBase to a delegate defined as public delegate object SampleDelegate(Derived b); because it would always be safe to call it, because it could only ever be called with a Derived argument, which could always be cast to Base on calling.

More often when we talk about covariance and contravariance with delegates in C#, we mean that of covariant and contravariant type parameters.

(Mainly because the type of variance described above was in the language since 2.0, and the type I'll describe below since 4.0, so the latter type was "news" to already-working C# programmers).

If we have a generic delegate defined as:

public delegate TResult MyDelegate<TArg, TResult>(TArg argument);

Then consider the following:

MyDelegate<Base, Derived> del = b => null;//simple example.
MyDelegate<Derived, Derived> del2 = del; // compiler error 1. CS0029: Cannot implicitly convert type
MyDelegate<Base, Base> del3 = del; // compiler error 2. CS0029: Cannot implicitly convert type
MyDelegate<Derived, Base> del4 = del; // compiler error 3. CS0029: Cannot implicitly convert type

We aren't allowed to do either of the two conversions, but when you think about it, what could possibly go wrong here?

If we change the definition of MyDelegate to:

public delegate TResult MyDelegate<in TArg, TResult>(TArg argument);

Then the first error goes away, because the in allows contravariance on TArg.

If we change the definition to:

public delegate TResult MyDelegate<TArg, out TResult>(TArg argument);

Then the second error goes away, because the out allows covariance on TResult.

Finally if we change the definition to:

public delegate TResult MyDelegate<in TArg, out TResult>(TArg argument);

Then all three errors go away, because we have both the in and the out.

The rules of covariance an contravariance won't allow you to assign anything illogical. E.g.:

public delegate TResult MyDelegate<out TArg, in TResult>(TArg argument);

Has two errors: We cannot expect to be safely covariant on TArg nor safely contravariant on TResult; if we were allowed to do this, we would be allowed to assign delegates that didn't work to other delegate types.

The Func and Action types provide examples of this. For example, one of them is defined as:

public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);

And hence we can assign a Func<object, object, string> to a variable of type Func<string, string, object> because all calls involved will work, but we cannot assign a Func<string, string, object> to a variable of type Func<object, object, string>, as this doesn't hold.

Generic covariance and contravariance also holds with interfaces, allowing us to e.g. assign an IEnumerable<string> to a variable of type IEnumerable<object> because everything we can call on an IEnumerable<object> we can safely call on an IEnumerable<string>.

Upvotes: 0

Eric Lippert
Eric Lippert

Reputation: 660327

If I understand the meaning of invariance correctly i should not be able to return object of Base type since my delegate mentions return type of object. Is my understanding of invariance wrong?

Since you can compile and run that program, you already know the answer to that question. Yes.

Let's ask the question you meant to ask:

Since the delegate is not even generic, clearly generic variance on delegates does not apply. Why then can I make a covariant conversion from a method returning Base to a delegate type that requires that the method return object?

Clearly generic covariance is not the kind of covariance that is relevant; there is an entirely different rule at play here. This conversion was first allowed in C# 2.0. When converting from a method group to a delegate, the method chosen from the method group may have a return type more general than the delegate's return type, provided that both types are reference types. And similarly for the parameter types, which are contravariant.

The feature of allowing conversions between generic delegate types constructed with reference types to similarly be covariant and contravariant was added -- by me, incidentally -- to C# 4.0.

Upvotes: 10

Related Questions