Freigeben über


Covariance and Contravariance in C#, Part Six: Interface Variance

Over the last few posts I’ve discussed how it is possible to treat a delegate as contravariant in its arguments and covariant in its return type. A delegate is basically just an object which represents a single function call; we can do this same kind of thing to other things which represent function calls. Interfaces, for example, are contracts which specify what set of function calls are available on a particular object.

This means that we could extend the notion of variance to interface definitions as well, using the same rules as we had for delegates. For example, consider

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

Here we have a generic interface where the sole use of the parameter is in an output position. We could therefore make the parameter covariant. That would mean that it would be legal to assign an object implementing IEnumerator<Giraffe> to a variable of type IEnumerator<Animal>. Since the user of that variable will always expect an Animal to come out, and the actual backing implementation always produces a Giraffe, everyone is happy.

Once we’ve got IEnumerator<+T>, we can then notice that IEnumerable<T> is defined as:

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

Again, the parameter appears only in an output position, so we could make IEnumerable<+T> covariant as well.

This then opens up a whole slew of nice scenarios. Today, this code would fail to compile:

void FeedAnimals(IEnumerable<Animal> animals) {
foreach(Animal animal in animals)
if (animal.Hungry)
Feed(animal);
}
...
IEnumerable<Giraffe> adultGiraffes = from g in giraffes where g.Age > 5 select g;
FeedAnimals(adultGiraffes);

Because adultGiraffes implements IEnumerable<Giraffe>, not IEnumerable<Animal>. With C# 3.0 you’d have to do a silly and expensive casting operation to make this compile, something like:

FeedAnimals(adultGiraffes.Cast<Animal>());

or

FeedAnimals(from g in adultGiraffes select (Animal)g);

Or whatever. This explicit typing should not be necessary. Unlike arrays (which are read-write) it is perfectly typesafe to treat a read-only list of giraffes as a list of animals.

Similarly, we could make

public interface IComparer<-T> {
int Compare(T x, T y);
}

into a contravariant interface, since the type parameter is used only in input positions. You could then implement an object which compares two Animals and use it in a context where you need an object which compares two Giraffes without worrying about type system problems.

Next time: Suppose we were to do interface and delegate variance in a hypothetical future version of C#. What would the syntax look like? Is this goofy plus and minus really the best we can do? Do we need any syntax at all?

Comments

  • Anonymous
    October 26, 2007
    The comment has been removed

  • Anonymous
    October 26, 2007
    The former. As I will discuss in my next post,  we need a way to say "this is covariant, this is invariant", etc. We will not be able to deduce the desired variance just from the interface definition, so the default will still be "invariant". Uh, hypothetically, of course.

  • Anonymous
    October 26, 2007
    Is it a binary behavior, i.e. could we collapse it to a single token specifying "this is variant in the only way that makes sense here"? Hypothetically.

  • Anonymous
    October 26, 2007
    An interesting idea -- however, as we'll see in my next post, the variance of one type parameter may depend upon the variance of another. There often is not one single "only way it makes sense here". There are often multiple ways that it could work, and we'd then have to choose.

  • Anonymous
    October 26, 2007
    I think one has to grasp the difference between covariance and contravariance to make any good use of it. Guessing variance from the interface would be dangerous even if you could do it, because the interface might change later, and this should not hurt existing code by removing or changing variance. The developer of the interface has to make it clear that he wants variance, so he will also be aware that certain changes might not be possible later (or would be breaking). During this blog series, I got used pretty well to the +/- notation, so why not. On the other hand, something more verbose like IEnumerable<covariant T> would not hurt either and be more c#-like. It's in declarations only anway. Would this, hypothetically, be for interfaces only, or can we have it for generic reference types too? This would be especially great because it would put an end to the "no common base type" problem of generics (well, not for value types), so instead of doing perverse reflection calls, we could just rely on object x = new List<string>(); Assert (x is IEnumerable<object>) (can't think of an example with classes right now, but I know I wanted it on several occations in the past) (one could argue that another way to approach this problem would be to clean up the mess that generics made of the reflection API. probably not easy, but definately due!) can't wait. what's the hypothetical timeline? ;-)

  • Anonymous
    October 27, 2007
    Would it be wrong to just avoid the covariant and contravariant checks?  With the complications you are showing, I think it is obvious that the parameter in a parametric type is orthogonal to the types of objects managed by that type. The illusion of contravariant only appears when you ASSUME the domain you assign a method is the ONLY domain the method can act on.  This assumption does not happen in all languages; a Javascript function can be designed to take a String, but can be passed any non-String.  The output of such a method is well defined: it outputs an exception.  With exceptions being accepted as part of a method's codomain, and realizing that all methods are well defined for all objects, then the issue of "contravariant on input parameters" disappears because all methods act on the same domain (everything). I believe little is lost in terms of optimization.   Object[] o=new Object[]; String[] s=o; //Allowed at compile time Assigning arrays, or any collection, can be checked at runtime when the contravariant relationship is broken.  But this can be done at the collection level, not at the individual element level, as long as there is no type-erasure.   Same for function delegates; checks can be done at runtime.  But I see no reason why simple constant propagation (constant TYPE propagation) would catch most illegal maneuvers at compile time anyway. Furthermore, method inheritance must be contravariant on the parameter types to be logically consistent in an OO framework.  And this works by delegating out-of-domain parameters back to the super class.  I believe the logic in such a proof can be mimicked with some kind of recursive function delegate; proving "contravariant on parameter types" is logically inconsistent in a function delegate framework.  But that is only a strong feeling.

  • Anonymous
    October 27, 2007
    Yeah, why bother with variance, haven't they heard of duck typing yet? Come on ... ;-)

  • Anonymous
    October 28, 2007
    I would love to have this kind of variance support. All too often I need to drop down to the IL level to do this kind of work. Thus, I am accustomed to the +- notation. However, perhaps your next post will have compelling reasons for another notation.

  • Anonymous
    October 28, 2007
    Stefan Wenig, I am not suggesting duck typing, I am suggesting strong dynamic typing of type parameters. Correction:  " Furthermore, method inheritance must be COVARIANT on the parameter types to be logically consistent in an OO framework."

  • Anonymous
    October 28, 2007
    The comment has been removed

  • Anonymous
    October 29, 2007
    To go back to a remark in one of the first posts: What we strongly lack is overriding a read only property by a more specialized one. More generically it would be convenient to define an overload of a method and to have some "OverrideAttribute(typeof(string))" to say e.g.  string Compute(object o) is ovverriding the base class implementation of object Compute(string s). In C# it could be expressed as the 'generic keyword' "override<string>". Covariance on return types woul be a gift, and covariance of IEnumerable<T> would definitely ease our lifes. After reading the CLR version 2 spec, and seeing covariance was not handled in Framework 2.0, I hoped it will be in 3.0. Then in 3.5. Try to make C# at least aware of what is specified in a delegate or interface, even if there is no language support to create it.

  • Anonymous
    December 23, 2008
    So nicely step by step blogged by Eric Lippert for &quot;Covariance and Contravariance&quot; as &quot;Fabulous