Common Type System—Co- and Contravariance


Jump to: navigation, search
Visual C# Tutorials
.NET Framework Tutorials

Common Type System

© 2006 Wiley Publishing Inc.

Co- and Contravariance

I’ve made some simplifications on the rules for binding up until this point. I stated that the return value and parameter types of the target method must match the delegate exactly in order to form a delegate instance over a target. This is technically incorrect. The CLR 2.0 permits so-called covariant and contravariant delegates (although 1.x did not). These terms are well defined in the field of computer science and are forms of type system polymorphism. Covariance means that a more derived type can be substituted where a lesser derived type is expected. Contravariance is the opposite—it means that a lesser derived type can be substituted where a further derived type was expected.

Covariant input is already permitted in terms of what a user can supply to a method. If your method expect BaseClass and somebody gives you an instance of DerivedClass (which subclasses BaseClass), the runtime permits it. This is bread and butter object-oriented polymorphism. Similarly, output can be contravariant in the sense that if the caller expects a lesser derived type, there is no harm in supplying an instance of a further derived type.

The topics of co- and contravariance get relatively complex quickly. Much of the literature says that contravariant input and covariant output is legal, which happens to be the exact opposite of what I just stated! This literature is usually in reference to the ability to override a method with a co-/contravariant signature, in which case it’s true. Derived classes can safely relax the typing requirements around input and tighten them around output if it deems it appropriate. Calling through the base version will still be type-safe. The CLR doesn’t natively support co- and contravariance in this manner.
For delegates, however, we are looking at the problem from a different angle: We’re simply saying that anybody who makes a call through the delegate might be subject to more specific input requirements and can expect less specific output. If you consider that calling a function through a delegate is similar to calling through a base class signature, this is the same policy.

As an example, consider the following definitions:

class A { /* ... */ }
class B : A { /* ... */ }
class C : B { /* ... */ }
B Foo(B b) { return b; }

If we wanted to form a delegate over the method Foo, to match precisely we’d need a delegate that returned a B and expected a B as input. This would look MyDelegate1 below:

delegate B MyDelegate1(B b);
delegate B MyDelegate2(C c);
delegate A MyDelegate3(B b);
delegate A MyDelegate4(C c);

But we can use covariance on the input to require that people calling through the delegate supply a more specific type than B. MyDelegate2 above demonstrates this. Alternatively, we could use contravariance to hide the fact that Foo returned a B, and instead make it look like it only returns an A, as is the case with MyDelegate3. Lastly, we could use both co- and contravariance simultaneously as shown with MyDelegate4. All four of these delegate signatures will bind to the Foo method above.


Previous_Page_.gif Next_Page_.gif





Personal tools