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?
Assume we have two variables
IInterface<Animal> i1 = anInterfaceAnimalValue;
IInterface<Cat> i2 = anInterfaceCatValue;
We can make these calls
i1.Baz(anAnimal, j => 5);
//this is the same as doing
i1.Baz(anAnimal, new IInterface<Animal>.Foo(j => 5));
i1.Baz(aCat, j => 5);
//this is the same as doing
i1.Baz(aCat, new IInterface<Animal>.Foo(j => 5));
i2.Baz(aCat, j => 5);
//this is the same as doing
i2.Baz(aCat, new IInterface<Cat>.Foo(j => 5));
If we now assign i1 = i2;
then what happens?
i1.Baz(anAnimal, j => 5);
//this is the same as doing
i1.Baz(anAnimal, new IInterface<Animal>.Foo(j => 5));
But IInterface<Cat>.Baz
(the actual object type) does not accept IInterface<Animal>.Foo
, it only accepts IInterface<Cat>.Foo
. The fact that these two delegates are the same signature does not take away from them being different types.
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).
Covariance and contravariance in generics
In general, a covariant type parameter can be used as the return type of a delegate, and contravariant type parameters can be used as parameter types. For an interface, covariant type parameters can be used as the return types of the interface's methods, and contravariant type parameters can be used as the parameter types of the interface's methods.
With type parameters of the arguments you pass in, it's somewhat confusing: if T
is covariant (output), a function can use void (Action<T>)
which looks like it's an input, and can accept a delegate which is more derived. It can also return Func<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):
II.9.1 Generic type definitions
The generic parameter is in scope in the declarations of:
- snip...
- all members (instance and static fields, methods, constructors, properties and events) except nested classes. [Note: C# allows generic parameters from an enclosing class to be used in a nested class, but adds any required extra generic parameters to the nested class definition in metadata. end note]
So nested types, of which the Foo
delegate is an example, actually don't have the generic T
type in scope. The C# compiler adds them in.
Now, see the following code, I have noted which lines do not compile:
public delegate void FooIn<in T>(T input);
public delegate T FooOut<out T>();
public interface IInterfaceIn<in T>
{
void BarIn(FooIn<T> input); //must be covariant
FooIn<T> BazIn();
void BarOut(FooOut<T> input);
FooOut<T> BazOut(); //must be covariant
public delegate void FooNest();
public delegate void FooNestIn(T input);
public delegate T FooNestOut();
void BarNest(FooNest input); //must be covariant
void BarNestIn(FooNestIn input); //must be covariant
void BarNestOut(FooNestOut input); //must be covariant
FooNest BazNest();
FooNestIn BazNestIn();
FooNestOut BazNestOut();
}
public interface IInterfaceOut<out T>
{
void BarIn(FooIn<T> input);
FooIn<T> BazIn(); //must be contravariant
void BarOut(FooOut<T> input); //must be contravariant
FooOut<T> BazOut();
public delegate void FooNest();
public delegate void FooNestIn(T input);
public delegate T FooNestOut();
void BarNest(FooNest input); //must be contravariant
void BarNestIn(FooNestIn input); //must be contravariant
void BarNestOut(FooNestOut input); //must be contravariant
FooNest BazNest();
FooNestIn BazNestIn();
FooNestOut BazNestOut();
}
Let's stick to IInterfaceIn
for the moment.
Take the invalid BarIn
. It uses FooIn
, whose type parameter is covariant.
Now, if we have anAnimalInterfaceValue
then we can call BarIn()
with a FooIn<Animal>
argument. This means that the delegate takes an Animal
argument. If we then cast it to IInterface<Cat>
then we could call it with a FooIn<Cat>
, which demands a parameter of type Cat
, and the underlying object is not expecting such a strict delegate, it expects to be able to pass any Animal
.
So BarIn
can therefore only use a type which is the same or less derived than what is declared, therefore it cannot receive the T
of IInterfaceIn
which may end being more derived.
BarOut
however, is valid because it uses FooOut
, which has a contra-variant T
.
Now let's look at FooNestIn
and FooNestOut
. These actually re-declare the T
parameter of the enclosing type. FooNestOut
is invalid because it uses the co-variant in T
in an output position. FooNestIn
is valid though.
Let's move on to BarNest
, BarNestIn
and BarNestOut
. 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 the in/out
of the respective paramtere):
II.9.7 Validity of member signatures
Given the annotated generic parameters
S = <var_1 T_1, ..., var_n T_n>
, we define what it means for various components of the type definition to be valid with respect toS
. We define a negation operation on annotations, written¬S
, to mean “flip negatives to positives, and positives to negatives”Methods. A method signature
tmeth(t_1,...,t_n)
is valid with respect toS
if
- its result type signature
t
is valid with respect toS
; and- each argument type signature
t_i
is valid with respect to¬S
.- each method generic parameter constraint type
t_j
is valid with respect to¬S
. [Note: In other words, the result behaves covariantly and the arguments behave contravariantly...
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With