# Tuesday, 10 August 2004
« Mono 1.0 | Main | JavaScript on IKVM »
Whidbey, Generics, Co- and contravariance

I came across an interesting Whidbey beta 1 CLR feature today. As far as I can tell this hasn't been documented anywhere yet, but the Whidbey beta 1 CLR supports covariant and contravariant generic type parameters.

This is a really interesting feature. If C# would support it (at the moment it doesn't), you'd be able to do something like this:

interface IReader<T>
{
  T Get(); 
}
interface IWriter<T>
{
  void Put(T t);
}
class Test<T> : IReader<T>, IWriter<T>
{
  T t;
  public T Get() { return t; }
  public void Put(T t) { this.t = t; }
}
class Driver
{
  static void Main()
  {
    Test<string> t1 = new Test<string>();
    t1.Put("covariance");
    IReader<object> rdr = t1;
    Console.WriteLine(rdr.Get());
    Test<object> t2 = new Test<object>();
    IWriter<string> wrtr = t1;
    wrtr.Put("contravariance");
    Console.WriteLine(t2.Get());
  }
}

Notice that in the first highlighted line we're assigning an object reference that implements IReader<string> to a local variable of type IReader<object>. This is covariance and it is type safe because the IReader<T> interface only returns T. In the second highlighted line we assign a reference that implements IWriter<object> to a local of type IWriter<string>, this is contravariance and this is also type safe, because IWriter<T> only accept arguments of type T.

Unfortunately, C# doesn't yet seem to support this feature, but if we manually write the IL, it'll run on the Whidbey beta 1 CLR (and it is fully verifiable):

.assembly extern mscorlib {}
.assembly test {}

.class interface public abstract
      'IReader`1'<+([mscorlib]System.Object) T>
{
  .method public abstract instance
          !T Get() cil managed {}
}

.class interface public abstract
      'IWriter`1'<-([mscorlib]System.Object) T>
{
  .method public abstract instance
          void Put(!T t) cil managed {}
}

.class public 'Test`1'<([mscorlib]System.Object) T>
      extends [mscorlib]System.Object
      implements class 'IReader`1'<!T>,
                 class 'IWriter`1'<!T>
{
  .field private !T v

  .method public virtual instance
          !T Get() cil managed
  {
    ldarg.0
    ldfld !0 class 'Test`1'<!T>::v
    ret
  }

  .method public virtual instance
          void Put(!T t) cil managed
  {
    ldarg.0
    ldarg.1
    stfld !0 class 'Test`1'<!T>::v
    ret
  }

  .method public specialname rtspecialname
          instance void .ctor() cil managed
  {
    ldarg.0
    call instance void [mscorlib]System.Object::.ctor()
    ret
  }
}

.method static void Main(string[] args) cil managed
{
  .entrypoint
  .locals init ([0] class 'Test`1'<string> t1,
                [1] class 'IReader`1'<object> rdr,
                [2] class 'Test`1'<object> t2,
                [3] class 'IWriter`1'<string> wrtr)

  newobj instance void class 'Test`1'<string>::.ctor()
  stloc.0
  ldloc.0
  ldstr "covariance"
  callvirt instance void class 'Test`1'<string>::Put(!0)
  ldloc.0
  stloc.1
  ldloc.1
  callvirt instance !0 class 'IReader`1'<object>::Get()
  call void [mscorlib]System.Console::WriteLine(object)

  newobj instance void class 'Test`1'<object>::.ctor()
  stloc.2
  ldloc.2
  stloc.3
  ldloc.3
  ldstr "contravariance"
  callvirt instance void class 'IWriter`1'<string>::Put(!0)
  ldloc.2
  callvirt instance !0 class 'IReader`1'<object>::Get()
  call void [mscorlib]System.Console::WriteLine(object)
  ret
}
 

Notice the plus and minus signs in the interface declarations to signal that a generic type parameter is covariant or contravariant.

Tuesday, 10 August 2004 16:03:46 (W. Europe Daylight Time, UTC+02:00)  #    Comments [6]
Tuesday, 10 August 2004 18:43:07 (W. Europe Daylight Time, UTC+02:00)
Interesting! Your sample C# doesn't give any special syntax for the covariant/contravariant interfaces - are you envisioning that the C# compiler would automatically infer where covariance or contravariance are appropriate and apply them automatically?

I can see how that could work for interfaces, but I'm not sure whether it could be done for classes. Seems like it would depend too much on what local variables of type T or T[] or Foo<T> etc are kept, what the covariance/contravariance of Foo<> is, and exactly what operations are done on those variables. Oh, and what private methods take Ts as parameters and return Ts.

I'm not sure, but I think even the base CLR and C# get this wrong for array types, by allowing string[] to be treated as object[] for example.

object[] arr = new string[1];
arr[0] = true;

Oops. No compile time errors or even warnings... if there was some special syntax for "readonly array of objects" and "writeonly array of objects" (maybe object[get] and object[set]), you could stop treating string[] as a subclass of object[], but still make it a subclass of object[get].

object[] arr1 = new string[1]; // illegal
object[get] arr2 = new string[1]; // fine
object[set] arr3 = new string[1]; // illegal
string[set] arr4 = new object[1]; fine
arr2[0] = true; // illegal, arr2 is marked for get only
string result = arr4[0]; // illegal, arr4 is marked for set only
object result = arr4[0]; // maybe legal - whatever array type arr4 really is, its elements are certainly subtypes of object...
Stuart
Tuesday, 10 August 2004 18:43:57 (W. Europe Daylight Time, UTC+02:00)
Welcome back to blogging, btw ;)
Stuart
Tuesday, 10 August 2004 18:49:16 (W. Europe Daylight Time, UTC+02:00)
>>Interesting! Your sample C# doesn't give any special syntax for the covariant/contravariant interfaces - are you envisioning that the C# compiler would automatically infer where covariance or contravariance are appropriate and apply them automatically?<<

I actually forgot to add some syntax. I don't think it would be a good idea to infer it automatically (that would create a huge versioning problem, if later on you decided to add a new method that would suddenly change the variance of a type).

>>I'm not sure, but I think even the base CLR and C# get this wrong for array types, by allowing string[] to be treated as object[] for example. <<

I agree this is stupid, but I'm glad they did it this way. Otherwise IKVM would be a lot harder ;-) Java does it this way too.

Tuesday, 10 August 2004 18:50:22 (W. Europe Daylight Time, UTC+02:00)
More spam, sorry ;) I get that way when something catches my interest...

I guess if you did want a special syntax for covariant and contravariant type parameters it might be possible to use get and set for that too:

public interface IReader<get T> {
T Get();
}

public interface IWriter<set T> {
void Put(T t);
}
Stuart
Tuesday, 10 August 2004 18:53:35 (W. Europe Daylight Time, UTC+02:00)
Seems like the object[]->string[] relationship might be one of those things that you might want to allow at the CLR level but forbid in C#, rather like non-public superclasses of public classes. Not sure whether this particular behavior is quite as easy to selectively forbid, though.
Stuart
Thursday, 12 August 2004 21:58:27 (W. Europe Daylight Time, UTC+02:00)
Finally convinced myself that it would be perfectly easy to allow this behavior in the CLR without requiring it in languages.

The CLR would need to handle Foo[], Foo[set] and Foo[get] as separate types with the appropriate "inheritance" relationships.

For languages like Java you'd just treat all arrays as if they were [get] types, since the "inheritance" of those is exactly the semantics that Java expects.

The CLR would allow array store instructions to put Foo objects into a Foo[get] array (even though this is an unsafe operation that might throw an ArrayStoreException), and permit casting operations to convert Foo[get] to Foo[] or Foo[set]. Languages like Java would never use Foo[] or Foo[set] types except for inter-language calls.

Languages that wanted to do it "right" would use all three types and handle them accordingly.
Stuart
Name
E-mail
Home page

I apologize for the lameness of this, but the comment spam was driving me nuts. In order to be able to post a comment, you need to answer a simple question. Hopefully this question is easy enough not to annoy serious commenters, but hard enough to keep the spammers away.

Anti-Spam Question: What method on java.lang.System returns an object's original hashcode (i.e. the one that would be returned by java.lang.Object.hashCode() if it wasn't overridden)? (case is significant)

Answer:  
Comment (HTML not allowed)  

Live Comment Preview