Tuesday, February 3, 2009

C# 4 is NOW!

Generic type variance support just landed in mcs. This is a C# 4 language feature.

You can give it a go by checking out SVN trunk and compiling your variant code with gmcs -langversion:future.

Well, this adds compiler support for variance but the Mono VM isn't up to speed on its variance handling. This means that you can compile the code but it won't actually run on Mono (until we fix that, which I am also doing). You can run it on the .NET 2.0 VM.

Generic type variance is like this:

Let's say I have some IEnumerable<string>, like so:

IEnumerable<string> myStrings = GetSomeStrings ();

Now let's say I have some other method which takes an IEnumerable<object>, like so:

void DoStuff (IEnumerable<object> someObjects)
    foreach (object o in someObjects) {
        // do some stuff with each object

POP QUIZ: Can I pass myStrings to DoStuff in C# 3? Strings are objects, right? And IEnumerable<T> is just a way of getting Ts. So if strings are objects, and IEnumerable<string> just returns strings, then we can also say that it returns objects. Just like IEnumerable<object>. So it should work, right?

ANSWER: Negatorz!

This is a problem of generic type variance. There are two kinds of variance: covariance and contravariance. The above example is covarant, meaning that you want to broaden the type of an output. Contravariance is the opposite: narrowing the type of an input. Let's consider a delegate:

delegate void Handler<T> (T input);

And let's say that we have some Handler<object>:

Handler<object> myHandler = delegate (object o) {
    // do something with the object

Now let's say that we have a method with takes a Handler<string>

void HandleStrings (Handler<string> handler, IEnumerable<string> strings)
    foreach (string s in strings) {
        handler (s);

We want to pass myHandler to HandleStrings. A Handler<object> takes objects, and strings are objects, so anything which is a Handler<object> should also be a legal Handler<string>. This is an example of contravariance.

It may surprise you to know, but the CLI has supported generic type variance since version 2. The rules are:
  • Variant type parameters are only allowed in interfaces and delegate types.

  • Contravariant type parameters can only be used as by-value method parameter types.

  • Covariant type parameters can only be used as method return types and generic arguments to inherited interfaces.

  • Only reference types are variant (this isn't explicitly stated in the spec, but it is the case).

  • Languages may choose to ignore variance and treat all generic parameters as invariant.

For whatever reason, the C# language team has so far chosen not to support generic type variance. Well, that will be changing in 2010. The preview given by Anders Hejlsberg at PDC '08 revealed that C# 4 will finally support variance. But who wants to wait? Especially considering that this has been a .NET VM feature since 2006. So you can now use this in gmcs if you pass -langversion:future.

Covariance (which, as you will remember, can only be used as a method return type or as a generic argument to an inherited interface) is denoted with the "out" keyword before the type parameter identifier:

interface IFoo<out T> : IBar<T>
    T Bat { get; }

Contravariance (which is legal only as the type of a by-value method parameter) is denoted with the "in" keyword:

interface IFoo<in T>
    void Bar (T bat);

So there you go! Now we just need to get Mono's VM variance support polished off. I'm sure I'll have good news for you about that shortly. Thanks goes to Marek Safar for reviewing patches. If you have questions about how or why variance works (it's kind of tricky to get your head around), leave a comment. I might do a post delving into all of the little rules behind variance.


Barry Kelly said...

FWIW, there are other kinds of variance. C# supports definition-site variance, rather than use-site variance like Java. There are also the covariant return types supported by C++ in overridden methods in descendants.

Scott said...

Yes, there are other variance schemes. I was just describing variance as it exists in .NET. Java generics has more advanced variance capabilities (wildcards and stuff) but it is not type-safe at the VM level. Generics in Java is a language feature only. When you actually compile down to byte-code, the generic type information is lost and the compiler injects casts on either side of generic members. This has a negative impact on performance and doesn't allow for generic arrays. It's a shame because they designed it that way so that they wouldn't need VM changes for Java 5 (which is a worthy goal), but then they ended up making VM changes anyway AND stuck with their crippled generics implementation. Poor Java.

Laurent Debacker said...

This is so great, my only deception with the .NET Framework's standard library was the cut between non-generic and generic collections: everytime I implement a IEnumerable< T> I must implement IEnumerable at the same time...
Since IEnumerator< T> IEnumerable< T>::GetEnumerator() should be covariant with IEnumerator< object> IEnumerable< object>::GetEnumerator() it should be aswell with IEnumerator IEnumerable::GetEnumerator(), solving part of my issue. But do you know if it works like that?

Scott said...

Variance only allows you to broaden or narrow a given generic type parameter in a generic type. IEnumerable<object> and IEnumerator<object> are both covariants of IEnumerable<string> and IEnumerator<string> respectively. But IEnumerable is not covariant of IEnumerable<T>. The current relationship between IEnumerable and IEnumerable<T> is an example of the Simulated Covariance Pattern (http://blogs.msdn.com/kcwalina/archive/2008/04/02/SimulatedCovariance.aspx). Unfortunately, .NET 1.0 did not ship with generics, so we're stuck with a lot of non-generic APIs ;)

Lucas said...

Great stuff! We target mono only, no clr, so we're very happy to see mono being right on the heels of the c# spec.