Consider the contravariant interface definition with a delegate:
public interface IInterface<in TInput>
{
delegate int Foo(int x);
void Bar(TInput input);
void Baz(TInput input, Foo foo);
}
The definition of Baz fails with an error:
CS1961
Invalid variance: The type parameter 'TInput' must be covariantly valid on 'IInterface<TInput>.Baz(TInput, IInterface<TInput>.Foo)'. 'TInput' is contravariant.
My question is why? On first glance this should be valid, as the Foo delegate has nothing to do with TInput. I don't know if it's the compiler being overly conservative or if I'm missing something.
Note that normally you wouldn't declare a delegate inside an interface, in particular this doesn't compile on versions older than C# 8, since a delegate in an interface needs default interface implementations.
Is there a way to break the type system if this definition was allowed, or is the compiler conservative?
TL;DR; This is correct according to the ECMA-335 spec, confusingly there are some situations when it does work
Assume we have two variables
We can make these calls
If we now assign
i1 = i2;then what happens?But
IInterface<Cat>.Baz(the actual object type) does not acceptIInterface<Animal>.Foo, it only acceptsIInterface<Cat>.Foo. The fact that these two delegates are the same signature does not take away from them being different types.Let's go into it a bit deeper
Let me preface this with two points:
Firstly, remember that co-variant generic types in interfaces can appear in output positions (this allows a more derived type), and contra-variant in input positions (allows a more base type).
With type parameters of the arguments you pass in, it's somewhat confusing: if
Tis covariant (output), a function can usevoid (Action<T>)which looks like it's an input, and can accept a delegate which is more derived. It can also returnFunc<T>.If
Tis contra-variant the opposite is true.See this excellent post by the great Eric Lippert and on the same question by Peter Duniho for further explanation on this point.
Secondly, ECMA-335, which defines the spec of the CLI, says the following (my bold):
So nested types, of which the
Foodelegate is an example, actually don't have the genericTtype in scope. The C# compiler adds them in.Now, see the following code, I have noted which lines do not compile:
Let's stick to
IInterfaceInfor the moment.Take the invalid
BarIn. It usesFooIn, whose type parameter is covariant.Now, if we have
anAnimalInterfaceValuethen we can callBarIn()with aFooIn<Animal>argument. This means that the delegate takes anAnimalargument. If we then cast it toIInterface<Cat>then we could call it with aFooIn<Cat>, which demands a parameter of typeCat, and the underlying object is not expecting such a strict delegate, it expects to be able to pass anyAnimal.So
BarIncan therefore only use a type which is the same or less derived than what is declared, therefore it cannot receive theTofIInterfaceInwhich may end being more derived.BarOuthowever, is valid because it usesFooOut, which has a contra-variantT.Now let's look at
FooNestInandFooNestOut. These actually re-declare theTparameter of the enclosing type.FooNestOutis invalid because it uses the co-variantin Tin an output position.FooNestInis valid though.Let's move on to
BarNest,BarNestInandBarNestOut. These are all invalid, because they use delegates which have a co-variant generic parameter. The key here is that we do not care if the delegate actually uses the type parameter in a necessary position, what we care about is whether the variance of the generic parameter of the delegate matches the type that we are supplying.Aha, you say, but then why do the
IInterfaceOutnested parameters not work?Let's look at ECMA-335 again, where it talks about generic parameters being valid, and asserts that each part of the generic type must be valid (my bold,
Srefers to a generic type e.g.List<T>,Tmeans a type parameter,varmeans thein/outof the respective paramtere):So we flip the variance of the type used in the method arguments.
The upshot of all this is that it is never valid to use a nested co- or contra-variant type in a method argument position, because the required variance is flipped, and therefore will not match. Whichever way round we do it, it won't work.
Conversely, using the delegate in the return position always works.