Why doesn’t delegate contravariance work with value types?

The answer given (that there is no variance involving value types) is correct. The reason covariance and contravariance do not work when one of the varying type arguments is a value type is as follows. Suppose it did work and show how things go horribly wrong:

Func<int> f1 = ()=>123;
Func<object> f2 = f1; // Suppose this were legal.
object ob = f2();

OK, what happens? f2 is reference-identical to f1. Therefore whatever f1 does, f2 does. What does f1 do? It puts a 32 bit integer on the stack. What does the assignment do? It takes whatever is on the stack and stores it in variable “ob”.

Where was the boxing instruction? There wasn’t one! We just stored a 32 bit integer into storage that was expecting not an integer but rather a 64 bit pointer to a heap location containing a boxed integer. So you’ve just both misaligned the stack and corrupted the contents of the variable with an invalid reference. Soon the process will go down in flames.

So where should the boxing instruction go? The compiler has to generate a boxing instruction somewhere. It can’t go after the call to f2, because the compiler believes that f2 returns an object that has already been boxed. It can’t go in the call to f1 because f1 returns an int, not a boxed int. It can’t go between the call to f2 and the call to f1 because they are the same delegate; there is no ‘between’.

The only thing we could do here is make the second line actually mean:

Func<object> f2 = ()=>(object)f1();

and now we don’t have reference identity between f1 and f2 anymore, so what is the point of variance? The whole point of having covariant reference conversions is to preserve reference identity.

No matter how you slice it, things go horribly wrong and there is no way to fix it. Therefore the best thing to do is to make the feature illegal in the first place; there is no variance allowed on generic delegate types where a value type would be the thing that is varying.

UPDATE: I should have noted here in my answer that in VB, you can convert an int-returning delegate to an object-returning delegate. VB simply produces a second delegate which wraps the call to the first delegate and boxes the result. VB chooses to abandon the restriction that a reference conversion preserves object identity.

This illustrates an interesting difference in the design philosophies of C# and VB. In C#, the design team is always thinking “how can the compiler find what is likely to be a bug in the user’s program and bring it to their attention?” and the VB team is thinking “how can we figure out what the user likely meant to happen and just do it on their behalf?” In short, the C# philosophy is “if you see something, say something”, and the VB philosophy is “do what I mean, not what I say”. Both are perfectly reasonable philosophies; it is interesting seeing how two languages that have almost identical feature sets differ in these small details due to design principles.

Leave a Comment