Jon Skeet said:
Not quite (IMO). The CLR supports covariance/contravariance by allowing
a type to be declared as List<Stream+> or List<Stream-> for instance.
Let's be clear in exactly what the CLR does and doesn't support, within
the spectrum of variance. I'll try and enumerate the different
possibilities, as far as I know them:
1) Covariance using inheritance, for return values / out-parameters (or
contravariance using inheritance, for in-arguments). This includes the
covariance supported by C++. If it were valid in C#, one could declare:
class Mammal
{
public virtual Mammal GetValue()
{
Console.WriteLine("Mammal::GetValue");
return null;
}
}
class Dog : Mammal
{
public override Dog GetValue()
{
Console.WriteLine("Dog::GetValue");
return null;
}
}
This will get an error in C#. It isn't supported in IL, because the
return type is part of the method specification, and there's no
"override" keyword in IL. Thus, the IL translation of the above would
define "Dog::GetValue" as a new, overloaded virtual method. Finally,
even though native C++ supports the construct, managed C++ does not. It
gives this error message:
---8<---
error C2392: 'Dog ^Dog::GetValue(void)' : covariant returns types are
not supported in managed types, otherwise 'Mammal
^Mammal::GetValue(void)' would be overridden
--->8---
2) Covariance / contravariance of generic parameters. The CLI spec (II
9.5) says that covariance and contravariance is supported for interfaces
and delegate types, but not for class or value types. The CLI support is
at the definition site, not the use site. The PDF I linked to doesn't
talk about the existing CLI support, but rather about a different kind
of variance - use-site variance. The CLI support is for definition-site
variance. This is different to Java 5 variance support, which supports
use-site variance.
So, neither List<-Stream> nor List<+Stream> can be declared. Instead,
the declaration would need to look like List<+T> or List<-T>. And even
then, List<> would need to be an interface, and what's more, the
covariant definition can't accept T values as input arguments for any of
its methods, and similarly the contravariant can't return T values.
---8<---
class Mammal { }
class Dog : Mammal { }
interface IReader<+T> // allows covariance
{
T GetValue();
}
interface IWriter<-T> // allows contravariance
{
void SetValue(T value);
}
--->8---
2a) Covariance of generic parameters.
This would allow the following, using the CIL syntax for covariance:
IReader<Dog> dogReader = null;
IReader<Mammal> mammalReader = dogReader;
2b) Contravariance of generic parameters.
This would allow:
IWriter<Mammal> mammalWriter = null;
IWriter<Dog> dogWriter = mammalWriter;
These are both disallowed in C#, but allowed at the IL level. Here's a
test IL file which assembles and passes PEVerify:
---8<---
..assembly extern mscorlib {}
..assembly Test {}
..class private auto ansi beforefieldinit Mammal
extends [mscorlib]System.Object {}
..class private auto ansi beforefieldinit Dog
extends Mammal {}
..class interface private abstract auto ansi IReader`1<+T>
{
.method public hidebysig newslot abstract virtual
instance !T GetValue() cil managed {}
}
..class interface private abstract auto ansi IWriter`1<-T>
{
.method public hidebysig newslot abstract virtual
instance void SetValue(!T 'value') cil managed {}
}
..class private auto ansi beforefieldinit App
extends [mscorlib]System.Object
{
.method private hidebysig static void Main() cil managed
{
.entrypoint
.locals init (
[0] class IReader`1<class Dog> dogReader,
[1] class IReader`1<class Mammal> mammalReader,
[2] class IWriter`1<class Mammal> mammalWriter,
[3] class IWriter`1<class Dog> dogWriter)
ldnull
stloc.0
ldloc.0
stloc.1
ldnull
stloc.2
ldloc.2
stloc.3
ret
}
}
--->8---
If one switches around the assignments, to try and treat covariance
contravariantly and vice versa, changing the body of the Main method to:
---8<---
ldnull
stloc.1
ldloc.1
stloc.0
ldnull
stloc.3
ldloc.3
stloc.2
ret
--->8----
One then gets the following errors from PEVerify:
---8<---
[IL]: Error: [App::Main][found ref 'IReader`1[Mammal]'][expected ref
'IReader`1[Dog]'] Unexpected type on the stack.
[IL]: Error: [App::Main][found ref 'IWriter`1[Dog]'][expected ref
'IWriter`1[Mammal]'] Unexpected type on the stack.
--->8---
Similarly, if one tries to make IReader<+T> contravariant, i.e. change
to IReader<-T>, and IWriter<-T> covariant, one gets the following
errors:
---8<---
[token 0x02000004] Type load failed.
[token 0x02000005] Type load failed.
--->8---
So, the covariant and contravariant support is there.
3) Covariance of arrays, the way C# and Java supports it.
This acts more or less like T[] was defined "Array<+T>", with the
addition of a run-time check for input arguments, which can only support
contravariance, not covariance.
That is a sort of *declarative* covariance/contravariance which can
tell the compiler that it *will* support one of GetValue or SetValue
(and which one
That's not the same as the runtime
covariance/contravariance of arrays in terms of compile-time type
safety, so it's worth making the distinction between them. The CLR
supports compile-time-unsafe covariance for arrays, but
compile-time-safe covariance/contravariance for generics. C# supports
compile-time-unsafe covariance for arrays, but has no concept of the
compile-time-safe generic covariance/contravariance supported by the
CLR.
Does that explain the distinction I make between the two sorts of
covariance any better?
It does, in so far as array covariance isn't provably sound - instead it
relies on runtime type checks. That's one clear demarcation.
-- Barry