Why Covariance and Contravariance Do Not Support Value Type

Why covariance and contravariance do not support value type

Basically, variance applies when the CLR can ensure that it doesn't need to make any representational change to the values. References all look the same - so you can use an IEnumerable<string> as an IEnumerable<object> without any change in representation; the native code itself doesn't need to know what you're doing with the values at all, so long as the infrastructure has guaranteed that it will definitely be valid.

For value types, that doesn't work - to treat an IEnumerable<int> as an IEnumerable<object>, the code using the sequence would have to know whether to perform a boxing conversion or not.

You might want to read Eric Lippert's blog post on representation and identity for more on this topic in general.

EDIT: Having reread Eric's blog post myself, it's at least as much about identity as representation, although the two are linked. In particular:

This is why covariant and contravariant conversions of interface and delegate types require that all varying type arguments be of reference types. To ensure that a variant reference conversion is always identity-preserving, all of the conversions involving type arguments must also be identity-preserving. The easiest way to ensure that all the non-trivial conversions on type arguments are identity-preserving is to restrict them to be reference conversions.

Why does C# use contravariance (not covariance) in input parameters with delegate?

Olivier's answer is correct; I thought I might try to explain this more intuitively.

Why does C# choose to use contravariance (not covariance) in input parameters in delegate?

Because contravariance is typesafe, covariance is not.

Instead of Base, let's say Mammal:

delegate void MammalDelegate(Mammal m);

This means "a function that takes a mammal and returns nothing".

So, suppose we have

void M(Giraffe x)

Can we use that as a mammal delegate? No. A mammal delegate must be able to accept any mammal, but M does not accept cats, it only accepts giraffes.

void N(Animal x)

Can we use that as a mammal delegate? Yes. A mammal delegate must be able to accept any mammal, and N does accept any mammal.

covariance/contravariance rule does not seem to work in this example.

There is no variance here to begin with. You are making the extremely common mistake of confusing assignment compatibility with covariance. Assignment compatibility is not covariance. Covariance is the property that a type system transformation preserves assignment compatibility.

Let me say that again.

You have a method that takes a Mammal. You can pass it a Giraffe. That is not covariance. That is assignment compatibility. The method has a formal parameter of type Mammal. That is a variable. You have a value of type Giraffe. That value can be assigned to that variable, so it is assignment compatible.

What then is variance, if it is not assignment compatibility? Let's look at an example or two:

A giraffe is assignment compatible with a variable of type mammal. Therefore a sequence of giraffes (IEnumerable<Giraffe>) is assignment compatible with a variable of type sequence of mammals (IEnumerable<Mammal>).

That is covariance. Covariance is the fact that we can deduce the assignment compatibility of two types from the assignment compatibility of two other types. We know that a giraffe may be assigned to a variable of type animal; that lets us deduce another assignment compatibility fact about two other types.

Your delegate example:

A mammal is assignment compatible with a variable of type animal. Therefore a method which takes an animal is assignment compatible with a variable of type delegate which takes a mammal.

That is contravariance. Contravariance is again, the fact that we can deduce the assignment compatibility of two things -- in this case a method can be assigned to a variable of a particular type -- from the assignment compatibility of two other types.

The difference between covariance and contravariance is simply that the "direction" is swapped. With covariance we know that A can be used as B implies that I<A> can be used as I<B>. With contravariance we know that I<B> can be used as I<A>.

Again: variance is a fact about the preservation of an assignment compatibility relationship across a transformation of types. It is not the fact that an instance of a subtype may be assigned to a variable of its supertype.

What other cases than delegate use covariance/contravariance and why?

  • Conversion of method groups to delegates uses covariance and contravariance on return and parameter types. This only works when the return / parameter types are reference types.

  • Generic delegates and interfaces may be marked as covariant or contravariant in their type parameters; the compiler will verify that the variance is always typesafe, and if not, will disallow the variance annotation. This only works when the type arguments are reference types.

  • Arrays where the element type is a reference type are covariant; this is not typesafe but it is legal. That is, you may use a Giraffe[] anywhere that an Animal[] is expected, even though you can put a turtle into an array of animals but not into an array of giraffes. Try to avoid doing that.

Note that C# does NOT support virtual function return type covariance. That is, you may not make a base class method virtual Animal M() and then in a derived class override Giraffe M(). C++ allows this, but C# does not.

UPDATE regarding the previous paragraph: This answer was written in 2016; in 2020, C# 9 does now support return type covariance.

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.

Problem understanding covariance contravariance with generics in C#

The error message is insufficiently informative, and that is my fault. Sorry about that.

The problem you are experiencing is a consequence of the fact that covariance only works on reference types.

You're probably saying "but IA is a reference type" right now. Yes, it is. But you didn't say that T is equal to IA. You said that T is a type which implements IA, and a value type can implement an interface. Therefore we do not know whether covariance will work, and we disallow it.

If you want covariance to work you have to tell the compiler that the type parameter is a reference type with the class constraint as well as the IA interface constraint.

The error message really should say that the conversion is not possible because covariance requires a guarantee of reference-type-ness, since that is the fundamental problem.

Covariance and Contravariance on the same type argument

As others have said, it is logically inconsistent for a generic type to be both covariant and contravariant. There are some excellent answers here so far, but let me add two more.

First off, read my article on the subject of variance "validity":

http://blogs.msdn.com/b/ericlippert/archive/2009/12/03/exact-rules-for-variance-validity.aspx

By definition, if a type is "covariantly valid" then it is not usable in a contravariant way. If it is "contravariantly valid" then it is not usable in a covariant way. Something that is both covariantly valid and contravariantly valid is not usable in either a covariant or contravariant way. That is, it is invariant. So, there is the union of covariant and contravariant: their union is invariant.

Second, let's suppose for a moment that you got your wish and that there was a type annotation that worked the way I think you want:

interface IBurger<in and out T> {}

Suppose you have an IBurger<string>. Because it is covariant, that is convertible to IBurger<object>. Because it is contravariant, that is in turn convertible to IBurger<Exception>, even though "string" and "Exception" have nothing whatsoever in common. Basically "in and out" means that IBurger<T1> is convertible to any type IBurger<T2> for any two reference types T1 and T2. How is that useful? What would you do with such a feature? Suppose you have an IBurger<Exception>, but the object is actually an IBurger<string>. What could you do with that, that both takes advantage of the fact that the type argument is Exception, and allows that type argument to be a complete lie, because the "real" type argument is an utterly unrelated type?

To answer your follow-up question: implicit reference type conversions involving arrays are covariant; they are not contravariant. Can you explain why you incorrectly believe them to be contravariant?

still confused about covariance and contravariance & in/out

Both covariance and contravariance in C# 4.0 refer to the ability of using a derived class instead of base class. The in/out keywords are compiler hints to indicate whether or not the type parameters will be used for input and output.

Covariance

Covariance in C# 4.0 is aided by out keyword and it means that a generic type using a derived class of the out type parameter is OK. Hence

IEnumerable<Fruit> fruit = new List<Apple>();

Since Apple is a Fruit, List<Apple> can be safely used as IEnumerable<Fruit>

Contravariance

Contravariance is the in keyword and it denotes input types, usually in delegates. The principle is the same, it means that the delegate can accept more derived class.

public delegate void Func<in T>(T param);

This means that if we have a Func<Fruit>, it can be converted to Func<Apple>.

Func<Fruit> fruitFunc = (fruit)=>{};
Func<Apple> appleFunc = fruitFunc;

Why are they called co/contravariance if they are basically the same thing?

Because even though the principle is the same, safe casting from derived to base, when used on the input types, we can safely cast a less derived type (Func<Fruit>) to a more derived type (Func<Apple>), which makes sense, since any function that takes Fruit, can also take Apple.

Type safety in CoVariance and ContraVariance

Covariance

Covariance is safe when SomeType only describes operations that return the type parameter

The IEnumerable<out T> interface is probably the most common example of covariance. It is safe because it only returns values of type T (well, specifically an IEnumerator<out T> but does not accept any T objects as parameters.

public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}

This works because IEnumerator<T> is also covariant and only returns T:

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
T Current { get; }
}

If you have a base class called Base and a derived class called Derived, then you can do stuff like this:

IEnumerable<Derived> derivedItems = Something();
IEnumerable<Base> baseItems = derivedItems;

This works because each item in derivedItems is also an instance of Base, so it is perfectly acceptable to assign it the way we just did. However, we cannot assign the other way:

IEnumerable<Base> baseItems = Something();
IEnumerable<Derived> derivedItems = baseItems; // No good!

This isn't safe because there is no guarantee that each instance of Base is also an instance of Derived.

Contravariance

Contravariance is safe when SomeType only describes operations that accept the type parameter

The Action<in T> delegate is a good example of contravariance.

public delegate void Action<in T>(T obj);

It is safe because it only accepts T as a parameter, but doesn't return T.

Contravariance lets you do stuff like this:

Action<Base> baseAction = b => b.DoSomething()
Action<Derived> derivedAction = baseAction;

Derived d = new Derived();
// These 2 lines do the same thing:
baseAction(d);
derivedAction(d);

This works because it is perfectly acceptable to pass an instance of Derived to baseAction. However, it doesn't work the other way around:

Action<Derived> derivedAction = d => d.DoSomething()
Action<Base> baseAction = derivedAction; // No good!

Base b = new Base();
baseAction(b); // This is OK.
derivedAction(b); // This does not work because b may not be an instance of Derived!

This isn't safe because there is no guarantee that an instance of Base will also be an instance of Derived.

Contravariance? Covariance? What's wrong with this generic architecture...?

Problem

Your problem is that you're mixing static types and runtime types: you're writing code that relies on having constructed generic types, but then you're invoking it with the base interface types.

Let's follow through your main flow:

Your CommandResolver always returns the static type ICommand. When you say:

var command = _commandResolver.GetCommand(message);
var handler = _handlerResolver.GetHandler(command);

The type of command is bound to ICommand and then gets passed to GetHander, which invokes GetHandler<ICommand>. That is, TCommand in this invocation is always bound to ICommand.

This is the main problem here. Since TCommand is always ICommand, doing:

_kernel.GetService(typeof(ICommandHandler<TCommand>))

...doesn't work (it looks for a ICommandHandler<ICommand> and the kernel doesn't have it); and even if it did work, you'd have to return it as ICommandHandler<ICommand> since that's the return type of the method.

By calling GetHandler without knowing (at compile time) the real type of the command, you lost the ability to use generics effectively and TCommand becomes meaningless.

So, you try to work around this: your resolver uses the command's runtime type(command.GetType()) to reflectively construct the type ICommandHandler<SomeCommandType> and tries to find that in the kernel.

Assuming you have something registered for that type, you'll get an ICommandHandler<SomeCommandType>, which you'll then try to cast to ICommandHandler<ICommand> (remember that TCommand is bound to ICommand). This of course won't work, unless TCommand is declared covariant in ICommandHandler<TCommand>, since you're casting up the type hierarchy; but even if it did, that's not what you want because what would you do with a ICommandHandler<ICommand> anyway?

Simply put: you can't cast an ICommandHandler<SomeCommand> to a ICommandHandler<ICommand> because that would imply that you can pass it any kind of ICommand and it'll happily handle it -- which is not true. If you want to use generic type parameters, you'll have to keep them bound to the real command type throughout the entire flow.

Solution

One solution to this problem is to keep TCommand bound to the real command type throughout the resolution of both the command and the command handler, e.g. by having something like FindHandlerAndHandle<TCommand>(TCommand command) and invoking it by reflection using the command's runtime type. But this is smelly and clumsy, and for a good reason: you're abusing generics.

Generic type-parameters are meant to help you when you know, in compile time, the type you want, or what you can unify it with another type parameter. In cases such as these, where you don't know that runtime type, trying to use generics only gets in your way.

A cleaner way to solve this is by separating the context when you know the command's type (when you write a handler for it) from the context when you don't know it (when you try to generically find a handler for a generic command). A good way to do that is by using an "untyped interface, typed base class" pattern:

public interface ICommandHandler // Look ma, no typeparams!
{
bool CanHandle(ICommand command);
void Handle(ICommand command);
}

public abstract class CommandHandlerBase<TCommand> : ICommandHandler
where TCommand : ICommand
{
public bool CanHandle(ICommand command) { return command is TCommand; }
public void Handle(ICommand command)
{
var typedCommand = command as TCommand;
if (typedCommand == null) throw new InvalidCommandTypeException(command);

Handle(typedCommand);
}

protected abstract void Handle(TCommand typedCommand);
}

This is a common way to bridge the generic and non-generic worlds: you use the non-generic interfaces when invoking them, but take advantage of the generic base-class when implementing. Your main flow now looks like this:

public void Handle(ICommand command)
{
var allHandlers = Kernel.ResolveAll<ICommandHandler>(); // you can make this a dependency

var handler = allHandlers.FirstOrDefault(h => h.CanHandle(command));
if (handler == null) throw new MissingHandlerException(command);

handler.Handle(command);
}

This is also somewhat more robust in the sense that the actual runtime type of the command doesn't have to match one-by-one with the type of the handler, so if you have a ICommandHandler<SomeBaseCommandType> it can handle commands of type SomeDerivedCommandType, so you can build handlers for intermediate base classes in the command type hierarchy, or use other inheritance tricks.

Covariance and contravariance not working when assigning anonymous method to delegate

To be the boring person who gives the answer "because that's what the spec says" (TL;DR at the end with my thoughts)...

Basically, it's because method group conversions (e.g. assigning a method to a delegate) and anonymous function conversions (e.g. assigning a lambda to a delegate) follow different rules, and only the former benefit from variance.

(Note that a Method Group means a group of 1 or more overloads of the same method - so your single methods still count as individual method groups)

Section 6.5 of the C# Language Specification talks about Anonymous function conversions:

An anonymous-method-expression or lambda-expression is classified as an anonymous function (§7.15). The expression does not have a type but can be implicitly converted to a compatible delegate type or expression tree type. Specifically, an anonymous function F is compatible with a delegate type D provided:

  • ...
  • If F has an explicitly typed parameter list, each parameter in D has the same type and modifiers as the corresponding parameter in F.

Section 6.6 however talks about Method group conversions:

An implicit conversion (§6.1) exists from a method group (§7.1) to a compatible delegate type. Given a delegate type D and an expression E that is classified as a method group, an implicit conversion exists from E to D if E contains at least one method that is applicable in its normal form (§7.5.3.1) to an argument list constructed by use of the parameter types and modifiers of D, as described in the following.

The compile-time application of a conversion from a method group E to a delegate type D is described in the following. Note that the existence of an implicit conversion from E to D does not guarantee that the compile-time application of the conversion will succeed without error.

  • A single method M is selected corresponding to a method invocation (§7.6.5.1) of the form E(A), with the following modifications:

    • The argument list A is a list of expressions, each classified as a variable and with the type and modifier (ref or out) of the corresponding parameter in the formal-parameter-list of D.
    • The candidate methods considered are only those methods that are applicable in their normal form (§7.5.3.1), not those applicable only in their expanded form.

So the method group -> delegate conversion uses more or less the same rules as if you had tried to invoke the method with the corresponding parameter types. We're directed to Section 7.6.5.1, which directs us to Section 7.5.3.1. This gets complex, so I'm not going to paste it verbatim here.

Interestingly, I couldn't find the section on delegate covariance, only interface covariance (although section 6.6. says mention it in an example).


TL;DR, when you write:

SampleDelegate test = SomeMethodGroup;

the compiler goes through a whole algorithm to pick a suitable member of the method group which is compatible with the delegate type, following much the same rules as overload resolution if you were invoking the method.

When you write:

SampleDelegate test = (First first) => new Second();

the compiler follows a much simpler rule of "does the lambda's signature match the delegate signature".

I suppose this makes sense. Most of the time you're going to be writing:

SampleDelegate test = first => new Second();

and it's up to the compiler to figure out the parameter types from the delegate signature. If you add in explicit types yourself, that doesn't completely change the algorithm used: the compiler uses the same algorithm, but if the types it comes up with conflicts with your explicit types, you get an error.


Note that almost all of the time this doesn't matter. It's rare to put types on a lambda's parameters, so you'd normally just write this:

SampleDelegate test = x => new Second();

The compiler infers that x actually a Second, and that's fine: if you've written a lambda which can work if x is a First, it should also work if x is a Second (LSP notwithstanding). Notice that you're allowed to get away with returning a Second even though SampleDelegate returns a first: the compiler doesn't mind.

Supporting both covariance and contravariance for a single type parameter

So why can't you suport both for a single type parameter?

Keep in mind that an interface can only be covariant in a type parameter if that type parameter is output-safe and an interface can only be contravariant in a type parameter if that type parameter is input-safe.

The syntax out T says that T is a covariant type parameter.

The syntax in T says that T is a contravariant type parameter.

As T is a covariant type parameter, it is by definition input-unsafe.

As T is a contravariant type parameter, it is by definition output-unsafe.

Therefore, T is input-unsafe and output-unsafe.

Consequently, T is prohibited in input positions, and T is prohibited in output positions.

Therefore, T can not appear in input positions nor in any output positions on any methods specified by the interface.

Consequently, T can not be used on the interface at all, and is pointless as a type parameter. Consequently, the language designers prohibit you from even including such a useless type marked as both covariant and contravariant on the interface to avoid the ugly

interface IFoo<in and out T> { }
Foo<T> : IFoo<T> { }

and then:

IFoo<Cat> cat = (IFoo<Animal>)new Foo<Dog>();

(If you need to read up on input-safe and output-safe, see 13.1.3.1 of the language specification.)



Related Topics



Leave a reply



Submit