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
T
is 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
T
is 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
Foo
delegate is an example, actually don't have the genericT
type 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
IInterfaceIn
for the moment.Take the invalid
BarIn
. It usesFooIn
, whose type parameter is covariant.Now, if we have
anAnimalInterfaceValue
then we can callBarIn()
with aFooIn<Animal>
argument. This means that the delegate takes anAnimal
argument. 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
BarIn
can therefore only use a type which is the same or less derived than what is declared, therefore it cannot receive theT
ofIInterfaceIn
which may end being more derived.BarOut
however, is valid because it usesFooOut
, which has a contra-variantT
.Now let's look at
FooNestIn
andFooNestOut
. These actually re-declare theT
parameter of the enclosing type.FooNestOut
is invalid because it uses the co-variantin T
in an output position.FooNestIn
is valid though.Let's move on to
BarNest
,BarNestIn
andBarNestOut
. 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
IInterfaceOut
nested 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,
S
refers to a generic type e.g.List<T>
,T
means a type parameter,var
means thein/out
of 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.