Generic Co- and Contravariance in Visual Basic 2010
Visual Studio 2010 has a new feature called generic co- and contravariance that is available when working with generic interfaces and delegates. In versions that precede Visual Studio 2010 and the Microsoft .NET Framework 4, generics behave invariantly with respect to subtyping, so conversions between generic types with different type arguments aren’t allowed.
For example, if you try passing a List(Of Derived) to a method that accepts an IEnumerable(Of Base), you’ll get an error. But Visual Studio 2010 can handle type-safe co- and contravariance that supports declaration of covariant and contravariant type parameters on generic interfaces and delegate type. In this article I’ll discuss what this feature really is and how you can take advantage of it in your applications.
Because a button is a control, you’d expect this code to work because of basic object-oriented inheritance principles:
It’s not allowed, though, in Visual Studio 2008, which will give the error “IEnumerable(Of Button) cannot be converted to IEnumerable(Of Control) .” But as object-oriented programmers, we know that a value of type Button can be converted to a control, so as noted earlier, according to basic inheritance principles, the code should be allowed.
Consider the following example:
This would fail with an InvalidCastException because the programmer converted the IList(Of Button) into an IList(Of Control), then inserted a control into it that wasn’t even a button.
Visual Studio 2010 recognizes and allows code like that in the first case, but can still disallow the type of code shown in the second case when targeting the .NET Framework 4. For most users, and the majority of the time, programs will just work in the expected way and there’ll be no need to dig deeper. In this article, though, I will dig deeper to explain how and why the code works.
In the first listing, which viewed an IEnumerable(Of Button) as an IEnumerable(Of Control), why was the code safe in Visual Studio 2010, while the second code sample, which viewed an IList(Of Button) as an IList(Of Control), unsafe?
The first is OK because IEnumerable(Of T) is an “out” interface, which means that in IEnumerable(Of Control), users of the interface can only take controls out of the list.
The second is unsafe because IList(Of T) is an “in-and-out” interface, so in IList(Of Control), users of the interface can put in controls as well as take them out.
The new language feature in Visual Studio 2010 that allows this is called generic covariance. In the .NET Framework 4, Microsoft has rewritten the framework along these lines:
The Out annotation in IEnumerable(Of Out T) indicates that if a method in IEnumerable mentions T, it will do so only in an out position, such as that for the return of a function or the type of a read-only property. This allows users to cast any IEnumerable(Of Derived) into an IEnumerable(Of Base) without running into an InvalidCastException.
IList lacks an annotation because IList(Of T) is an in-and-out interface. As a result, users can’t cast IList(Of Derived) into IList(Of Base) or vice versa; doing so could lead to an InvalidCastException, as you saw above.
There’s a mirror of the Out annotation. It’s a bit more subtle, so I’ll start with an example:
Here I’ve created a comparer that can determine whether any two controls are the same, which it does by looking at their names only.
Because the comparer can compare any controls at all, it certainly has the ability to evaluate two controls that happen to be buttons. That’s why you can safely cast it to an IComparer(Of Button). In general, you can safely cast an IComparer(Of Base) into any IComparer(Of Derived). This is called contravariance. It’s done with In annotations and is the exact mirror image of the Out annotations.
The .NET Framework 4 has also been modified to incorporate In generic type parameters:
Because of the IComparer(Of T) In annotation, every method in IComparer that mentions T will do so only in an in position such as that of a ByVal argument or the type of a write-only property. Thus users can cast an IComparer(Of Base) into any IComparer(Of Derived) without running into an InvalidCastException.
Let’s consider an example of the Action delegate in .NET 4 where the delegate becomes contravariant in T:
The example works in .NET 4 because the user of the delegate actionButton will always invoke it with Button arguments, which are controls.
You can add In and Out annotations to your own generic interfaces and delegates as well. But because of common language runtime (CLR) limitations, you can’t use these annotations on classes, structures or anything else. In short, only interfaces and delegates can be co- or contravariant.
Visual Basic uses two new contextual keywords: Out, which introduces covariance, and In, which does the same for contravariance, as illustrated in this example:
Why do we need these two contextual keywords or the syntax at all, though? Why don’t we infer variance In/Out automatically? First, it’s useful for programmers to declare their intent. Second, there are places where the compiler can’t infer the best variance annotation automatically.
Let’s consider two interfaces, IReadWriteBase and IReadWrite:
If the compiler infers them both to be Out, as below, the code works fine:
And if the compiler infers both to be In, as shown here, again the code works fine:
The compiler can’t know which one to pick—In or Out—so it provides a syntax.
Out/In contextual keywords appear in interface and delegate declarations only. Using the keywords in any other generic parameter declaration will cause a compile-time error. The Visual Basic compiler doesn’t allow a variant interface to contain nested enumerations, classes and structures because the CLR doesn’t support variant classes. You can, however, nest variant interfaces inside a class.
Dealing with Ambiguity
Co- and contravariance introduce ambiguity in member lookup, so you should know what triggers ambiguity and how the Visual Basic compiler handles it.
Let’s consider the example in Figure 1, in which we try to convert Comparer to IComparer(Of Control) where Comparer implements IComparer(Of Button) and IComparer(Of CheckBox).
Figure 1 An Ambiguous Conversion
Because both IComparer(Of Button) and IComparer(Of CheckBox) are variant-convertible to IComparer(Of Control), the conversion will be ambiguous. As a result, the Visual Basic compiler looks for ambiguities according to the CLR rules, and if Option Strict is On, disallows such ambiguous conversions at compile time; if Option Strict is Off, the compiler generates a warning.
The conversion in Figure 2 succeeds at runtime and doesn’t generate a compile-time error.
Figure 2 A Conversion that Succeeds at Runtime
Figure 3 illustrates the danger of implementing both IComparer(of IBase) and IComparer(of IDerived) generic interfaces. Here, Comparer1 and Comparer2 classes implement the same variant generic interface with different generic type parameters in different order. Even though Comparer1 and Comparer2 are identical, apart from ordering when they implement the interface, the call to the Compare method in those classes gives different results.
Figure 3 Different Results from the Same Method
Here’s why different results emerge from the code in Figure 3, even though _comp and _comp2 are identical. The compiler just emits Microsoft Intermediate Language that performs the cast. Thus the choice of whether to get the IComparer(Of IAccountRoot) or IComparer(Of IAccount) interface, given an implementation of the Compare() method that’s different, falls to the CLR, which always picks the first assignment-compatible interface in the list of interfaces. So with the code in Figure 3, the Compare() method gives different results because the CLR chooses the IComparer(Of IAccountRoot) interface for Comparer1 class and the IComparer(Of IAccount) interface for Comparer2 class.
Constraints on a Generic Interface
When you write a generic constraint—(Of T As U, U)—As now encompasses variance-convertibility in addition to inheritance. Figure 4 demonstrates that (of T As U, U) encompasses variance-convertibility.
Figure 4 How a Generic Constraint Encompasses Variance-Convertibility
A variant generic parameter can be constrained to a different variant parameter. Consider this:
In this example, an IEnumerator(Of ButtonBase, ButtonBase) can be variance-converted to an IEnumerator(Of Control, Button), and IEnumerable(Of Control, Button) can be converted to IEnumerable(Of ButtonBase, ButtonBase) with the constraints still satisfied. Notionally, it could be further variance-converted to IEnumerable(Of ButtonBase, Control), but this no longer satisfies the constraints, so it isn’t a valid type. Figure 5 represents a first-in, first-out collection of objects where a constraint could be useful.
Figure 5 Where a Constraint Is Useful in a Collection of Objects
In Figure 5, if I give _IPipe to you, you can only push Button into the pipe, and you can only read Control from it. Note that you can constrain a variant interface to a value type, given that the interface will never allow a variance conversion. Here’s an example with value type constraint in a generic parameter:
Constraint to value type might be useless, given that variance conversion is not allowed on a variant interface instantiated with value types. But note that with Tout being a structure, it might be useful to infer the type indirectly through constraints.
Constraints on a Function’s Generic Parameters
Constraints in methods/functions must have In types. Here are two basic ways of thinking about constraints on a function’s generic parameters:
Let’s consider Figure 6, which makes clear what would happen without this variance-validity rule on constraints.
Figure 6 What Happens Without a Variance-Validity Rule on Constraints
In Figure 6, we’ve stored a Label inside m_data as Button, which is illegal. Therefore, constraints in methods/functions must have In types.
Overloading refers to creating multiple functions with the same name that take different argument types. Overload resolution is a compile-time mechanism for selecting the best function from a set of candidates.
Let’s take a look at the following example:
What actually happens behind the scenes here? When the compiler sees the call to Foo(2), it has to figure out which Foo you want to invoke. To do so, it uses the following simple algorithm:
With the introduction of variance, a set of predefined conversions is expanded, and as the result, Step 2 will accept more candidate functions than there were before. Also, in cases where there used to be two equally specific candidates, the compiler would have picked the unshadowed one, but now the shadowed one may be wider, so the compiler may pick it instead. Figure 7 demonstrates code that could potentially break with the addition of variance into Visual Basic.
Figure 7 Code that Might Break with the Addition of Variance into Visual Basic
In Visual Studio 2008, the call to Add would bind to Object, but with Visual Studio 2010’s variance-convertibility, we use IEnumerable(Of Control) instead.
The compiler picks a narrowing candidate only if there’s no other, but with variance-convertibility, if there’s a new widening candidate, the compiler picks it instead. If variance-convertibility makes another new narrowing candidate, the compiler emits an error.
Extension methods enable you to add methods to existing types without creating a new derived type, recompiling or otherwise modifying the original type. In Visual Studio 2008, extension methods support array covariance, as in the following example:
But in Visual Studio 2010, extension methods also dispatch on generic variance. This may be a breaking change, as shown in Figure 8, because you may have more extension candidates than before.
Figure 8 A Breaking Change
Visual Basic allows you to declare conversions on classes and structures so that they can be converted to or from other classes and structures as well as basic types. In Visual Studio 2010, variance-convertibility is already added into user-defined conversion algorithms. Therefore, the scope of every user-defined conversion will increase automatically, which might introduce breaks.
Because Visual Basic and C# don’t allow user-defined conversions on interfaces, we need worry only about delegate types. Consider the conversion in Figure 9, which works in Visual Studio 2008 but causes an error in Visual Studio 2010.
Figure 9 A Conversion that Works in Visual Studio 2008 but Produces an Error in Visual Studio 2010
Figure 10 gives another example of a conversion that would cause a compile-time error in Visual Studio 2008 with Option Strict On, but will succeed in Visual Studio 2010 with variance-convertibility.
Figure 10 Visual Studio 2010 Allows a Formerly Illegal Conversion by Using Variance Convertibility
Effects of Option Strict Off
Option Strict Off normally allows narrowing conversions to be done implicitly. But whether Option Strict is On or Off, variance-convertibility requires its generic arguments to be related through the CLR’s assignment-compatible widening; it’s not enough for them to be related through narrowing (see Figure 11). Note: We do count T->U as narrowing if there’s a variance conversion U->T, and we count T->U as narrowing if T->U is ambiguous.
Figure 11 With Option Strict Off
Restrictions on Co-and Contravariance
Here’s a list of restrictions with co- and contravariance:
More-Flexible, Cleaner Code
When working with generics, in some cases you may have known you could have written simpler or cleaner code if co- and contravariance had been supported. Now that these features are implemented in Visual Studio 2010 and the .NET Framework 4, you can make your code much cleaner and more flexible by declaring variance properties on type parameters in generic interfaces and delegates.
To facilitate this, in the .NET Framework 4, IEnumerable is now declared covariant using the Out modifier in its type parameter, and IComparer is declared contravariant using the In modifier. So for IEnumerable(Of T) to be variance-convertible to IEnumerable(Of U), you have to have one of the following conditions:
In the Basic Class Library, these interfaces are declared as follows:
To be type-safe, covariant type parameters can appear only as return types or read-only properties (for example, they can be result types, as in the GetEnumerator method and Current property above); contravariant type parameters can appear only as parameter or write-only properties (argument types, for instance, as in the Compare and CompareTo methods above).
Co- and contravariance are interesting features that eliminate certain inflexibilities when working with generic interfaces and delegates. Having some basic knowledge about these features can be very helpful when writing code that works with generics in Visual Studio 2010.
Binyam Kelile is a software design engineer in Test with the Microsoft Managed Language Team. During the VS 2008 release, he worked on many of the language features, including LINQ Queries and Lexical Closure. For the upcoming Visual Studio release, he worked on co- and contravariance features. You can reach him at email@example.com.
Thanks to the following technical experts for reviewing this article: Beth Massi, Lucian Wischik