Compartilhar via


Covariance and Contravariance in C#, Part Ten: Dealing With Ambiguity

OK, I wasn’t quite done. One more variance post!

Smart people: suppose we made IEnumerable<T> covariant in T. What should this program fragment do?

class C : IEnumerable<Giraffe>, IEnumerable<Turtle> {
IEnumerator<Giraffe> IEnumerable<Giraffe>.GetEnumerator() {
yield return new Giraffe();
}
IEnumerator<Turtle> IEnumerable<Turtle>.GetEnumerator() {
yield return new Turtle();
}
// [etc.]
}

class Program {
static void Main() {
IEnumerable<Animal> animals = new C();
Console.WriteLine(animals.First().GetType().ToString());
}
}

Options:

1) Compile-time error.
2) Run-time error.
3) Always enumerate Giraffes.
4) Always enumerate Turtles.
5) Choose Giraffes vs Turtles at runtime.
6) Other, please specify.

And if you like any options other than (1), should this be a compile-time warning?

Comments

  • Anonymous
    November 09, 2007
  1. Compile time error. Also, object o = new C(); var e = (IEnumerable<Animal>) o; should be a runtime error. Some sticky situations are harder, though. For example, is it possible to make this work? IEnumerable<Animal> a1 = (IEnumerable<Giraffe>) new C(); IEnumerable<Animal> a2 = (IEnumerable<Turtle>) new C(); Console.WriteLine(a1.First().GetType().ToString()); // Giraffe Console.WriteLine(a2.First().GetType().ToString()); // Turtle
  • Anonymous
    November 09, 2007
    There is no reasonably obvious expected behavior if that code were to compile, so it would have to be a compile time error.

  • Anonymous
    November 09, 2007
    My uneducated guess is 3) Always enumerate Giraffes. Being as C inherits from IEnumerable<Giraffe> first, this is the less ambiguous option.

  • Anonymous
    November 09, 2007
    The comment has been removed

  • Anonymous
    November 09, 2007
    Absolutely compile-time. Anything else would be super-confusing.

  • Anonymous
    November 09, 2007
    In the example above, didn't the programmer mean IEnumberable<Animal>? Or, perhaps IEnumerable<T> where T : Giraffe, Turtle? But, assuming this is a more legitimate case of two IEnumerable<> types, it seems the compiler should support this. (Not a compile time error.) The LHS is requesting (in the example) a very specific interface. The IEnumerable<Turtle> interface. At compile time, it seems plausible that this type could be deduced. In fact, you should be able to follow this up the heirarchy to IEnumerable<Animal> and so on. Chuck (crystal_bit@hotmail.com)

  • Anonymous
    November 09, 2007
    The comment has been removed

  • Anonymous
    November 09, 2007
    The comment has been removed

  • Anonymous
    November 09, 2007
    I would expect to respond in the same way it responds with any other ambiguity: compile-error. This is an interesting scenario.  What you're essentially allowing programmers to implement would be similar to: interface IBase { int Method ( ); } interface IOne : IBase { } interface ITwo : IBase { } class OneClass : IOne, ITwo { int IOne.Method ( ) { return 1; } int ITwo.Method ( ) { return 2; } } ...which, of course, is not syntactically correct.

  • Anonymous
    November 09, 2007
    Additional question (I guess it's really the same question as the vtable implementation): Is an assignment of an IEnumerable<Giraffe> to an IEnumerable<Animal> variable statically verifyable, i.e. is it just copying a DWORD at runtime, or do you have to check or even convert the assignment? Either way, I'd argue that implementing IEnumerable<T> for Giraffe AND Turtle should break covariance. I noted that the CLR currently chooses option 3 (first interface implemented), and I really think this is dangerous. I know I'm asking a lot for breaking changes, this looks wrong. I say discourage these implementations (using warnings), and break covariance if the programmer insists.

  • Anonymous
    November 09, 2007
    Exactly the same problem already exists with user-defined conversions: class Program { public static void Main(string[] args) { Animal a = new SomeClass(); Console.WriteLine(a.GetType().Name); } } abstract class Animal { } class Giraffe : Animal { } class Turtle : Animal { } class SomeClass { public static implicit operator Giraffe(SomeClass c) { return new Giraffe(); } public static implicit operator Turtle(SomeClass c) { return new Turtle(); } } It results in a compile time error (ambiguous user-defined operators SomeClass->Giraffe and SomeClass->Turtle), so that's what should happen for the covariant IEnumerable, too.

  • Anonymous
    November 09, 2007
    The comment has been removed

  • Anonymous
    November 09, 2007
    Definitely 1 (compile-time error). Please avoid confusion whenever possible. We don't want programming to become gambling, do we?

  • Anonymous
    November 10, 2007
    The comment has been removed

  • Anonymous
    November 10, 2007
    The comment has been removed

  • Anonymous
    November 11, 2007
    Another vote for compile-time error. It's pretty rare to see so much agreement on a language topic - I think the consensus is clear on this one :) Jon

  • Anonymous
    November 11, 2007
    It depends on your First method, which you omitted. I am away from the VS2008 beta at the moment. Is it an extension method on IEnumerable<T>? Is it supposed to return the first implemented type? That seems a little ridiculous, but that is is up to the definition of the method. Besides the assignability of C to IEnumerable<Animal> I do not think that this example has any relevance on variance. The fact that C simultaneously implements IEnumerable<Giraffe> and IEnumerable<Turtle> seems to be a poor design, but from the language point of view it is certainly legal regardless of variance. There may even be appropriate uses for it (I do not know). Thus, I will have to choose "6) Other, please specify": clean compile, no error, warning, or run-time exception. Depending on the definition of First, either Giraffe or Turtle would be acceptable. If you can explain the definition of First, I will try to give a better answer.

  • Anonymous
    November 11, 2007
    First extends IEnumerable<T> and returns a T - the first one in the sequence. So, which would it return? A Turtle or a Giraffe? I can't see how it could do anything other than be a compilation error. Picking one sequence over the other arbitrarily would be disasterous, IMO. Jon

  • Anonymous
    November 11, 2007
    What Peter Ritchie said. You're providing two different IEnumerable<Animal> implementations, so the user has to specify which one they want (by casting the C to IEnumerable<T> for T in {Giraffe, Turtle}).

  • Anonymous
    November 12, 2007
    I wonder if it would be possible to return a "multi-type" variable?  Ie// one that is either a Giraffe or a Turtle, but never a "Lion" or an "Alligator".   To me, at least, this code is trying to tell you that the container can hold only Giraffes and Turtles and nothing else from the set of Animals (perhaps because storing Giraffes and Lions in the same container leads to bad results).  Enumerating through the container should get a single aggregate set of both Turtles and Giraffes, depending upon what was stored in there.  Trying to store a Lion in that code should fail at compile time but adding either Turtles or Giraffes should be just fine. In that conceptual model, the "First().GetType()" call should return a hybrid type of "Giraffe" | "Turtle" in the generic sense, but a specific "Giraffe" or "Turtle" in the specific case depending upon the type stored in the first element. Is there a better way to express this programatic desire?  Currently, the only way to put N siblings together is to either implement a seperate container for each, to include the base class or to implement some middle "GirraffeAndTurtle" class which inherits from "Animal" and from which both Turlte and Giraffe are derived.  It would be nice to have something clearer. Maybe something notationally like: Class C: IEnumerable<Turtle | Giraffe> { IEnumerator<Giraffe | Turtle> IEnumerable<Giraffe | Turtle>.GetEnumerator() {       if (current_obj is Giraffe)        yield return new Giraffe(current_obj);       if (current_obj is Turtle)         yield return new Turtle(current_obj);       else throw new IncompatibleAnimalException();    } } class Program {    static void Main()  {        #success        IEnumerable<Animal+> animals = new C();        #All the "giraffes"' stored in the set        IEnumerable<Giraffe> giraffes = new C();        #All the "Turtles" stored in the set        IEnumerable<Turtle> turtles = new C();        # Breaks at compile time        IEnumerable<Lion> lions = new C():        Console.WriteLine(animals.First().GetType().ToString());    } }

  • Anonymous
    November 12, 2007
    James: if there were a "multi-type" return, what would it mean to call its "Reproduce" method, considering the Giraffe is a mammal that births its young and the turtle is a reptile that lays eggs.  At some point you need to tell the compiler which of the two types you want to deal with.

  • Anonymous
    November 12, 2007
    The comment has been removed

  • Anonymous
    November 12, 2007
    First is an extension method which takes an IEnumerable<T>, calls GetEnumerator, and returns the first T in the sequence.  If there is no first T then it throws an exception.

  • Anonymous
    November 12, 2007
    The "First" is irrelevant. If it helps, suppose I had written foreach(Animal in animals) Console.WriteLine(animal.GetType().ToString()); Should this code compile? Should it give a warning?  What output should it produce? Now suppose instead of        IEnumerable<Animal> animals = new C(); I'd said        object x = new C();        IEnumerable<Animal> animals = (IEnumerable<Animal>)x; foreach(Animal in animals) Console.WriteLine(animal.GetType().ToString()); Should this code compile? Should it give a warning?  What output should it produce?

  • Anonymous
    November 12, 2007
    Considering Eric's clarification and new example, error for both cases. More specifically, CS1640 seems appropriate. Why? Because it is the same issue as in existing implementations and has nothing to do with variance. Don't over think it. The only variance issue here is the acceptability of assigning C to to IEnumerable<Animal>.

  • Anonymous
    November 13, 2007
    How would the compiler know to give an error for the second case? The conversion from C to object is unambiguous, as is the conversion from object to IE<Animal>.

  • Anonymous
    November 13, 2007
    The comment has been removed

  • Anonymous
    November 13, 2007
    Ben, because you're converting to the IEnumerable<Animal> type first, the compiler will know which GetEnumerator to use.  The same happens in C# now: public class C : System.Collections.IEnumerable, IEnumerable<int>, IEnumerable<string> {    IEnumerator<int> IEnumerable<int>.GetEnumerator()    {        yield break;    }    IEnumerator<string> IEnumerable<string>.GetEnumerator()    {        yield break;    }    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()    {        return (System.Collections.IEnumerator)((IEnumerable<string>)this).GetEnumerator();    } } public class Test {    public static int Main()    {        IEnumerable<int> e = new C();        foreach (int i in e){}    // no error CS1640        return 1;    } }

  • Anonymous
    November 13, 2007
    Peter Ritchie, The difference though is that in your example, C does in fact implement IEnumerable<int>. Thus, it knows exactly which one to bind to. Changing e to C e = new C() will of course produce CS1640. > because you're converting to the IEnumerable<Animal> type first, the compiler will know which > GetEnumerator to use. It does? And which one would that be? There is no IEnumerable<Animal>.GetEnumerator() that returns an IEnumerator<Animal>. It can only return an IEnumerator<Giraffe> or IEnumerator<Turtle>, both of which may be treated as an IEnumerator<Animal> and thus the ambiguituy: CS1640.

  • Anonymous
    November 13, 2007
    Ben, the example code was pulled from the documentation for CS1640 and slightly modified.  This is the original code: public class C : IEnumerable, IEnumerable<int>, IEnumerable<string> {    IEnumerator<int> IEnumerable<int>.GetEnumerator()    {        yield break;    }    IEnumerator<string> IEnumerable<string>.GetEnumerator()    {        yield break;    }    IEnumerator IEnumerable.GetEnumerator()    {        return (IEnumerator)((IEnumerable<string>)this).GetEnumerator();    } } public class Test {    public static int Main()    {        foreach (int i in new C()){}    // CS1640        // Try specifing the type of IEnumerable<T>        // foreach (int i in (IEnumerable<int>)new C()){}        return 1;    } } In Eric's example, if he first converts to a IEnumerable<Animal> object, that object must have an IEnumerable<Animal> GetEnumerator() method so foreach knows exactly what GetEnumerator() to use, just as it knows in the IEnumerable<int>/IEnumerable<string> example because I forced it to use the IEnumerable<int> interface.  If you don't tell it what type of IEnumerable<T> to use, yes, the compiler thankfully won't make the decision for you and spits out CS1640; but that's not the case the Eric presented.

  • Anonymous
    November 14, 2007
    > There is no IEnumerable<Animal>.GetEnumerator() that returns an IEnumerator<Animal>. It can only return an IEnumerator<Giraffe> or IEnumerator<Turtle>, both of which may be treated as an IEnumerator<Animal> and thus the ambiguituy: CS1640. OK, but how does the compiler know that?  Suppose I split the code into three assemblies: assembly #1: public class D { public static void M(IEnumerable<Animal> animals) { foreach(Animal a in .... assembly #2: public class E { public static void N(object x) { D.M((IEnumerable<Animal>)x); } } assembly #3: public class C : IEnumerable<Giraffe>, IEnumerable<Turtle> { ... } public class F { public static void P() { E.N(new C()); } } I compile assembly one, then assembly two, then assembly three.  In which one do I get a compilation error?

  • Anonymous
    November 14, 2007
    The comment has been removed

  • Anonymous
    November 14, 2007
    > OK, but how does the compiler know that? Is there no way for the compiler to determine the exact type that something points at? If not, then it may need to be a run-time error. However, before that, in that case, it would fail when it looks for and can not find IEnumerator<Animal> IEnumerable<Animal>.GetEnumerator() in animals (of type IEnumerable<Animal>, but really pointing to C). That fact that C can also provide a IEnumerator<Giraffe> and IEnumerator<Turtle> which could pass as an IEnumerator<Animal> is immaterial if the compiler can not determine that animals is really an instance of C. Again, this would be a compile-time error, but not due to variance but rather because the compile can not determine that animals (IEnumerator<Animal>) is really a C instance.

  • Anonymous
    November 15, 2007
    It would be a terrible idea for C# to depend on the order of interfaces in the "inheritance list".  This would be like depending on the order attributes are applied.  I would be shocked if it weren't a rule for C# design to avoid making ordering in such cases matter.  There's just too much room for error. The type-casting logic needs to take into account the diamond problem and raise a compile-time error when possible, otherwise a run-time exception.  I see no reasonable way for the compiler to know how to choose IEnumerable<Turtle> over IEnumerable<Giraffe>.  If the author of the class wants to support IEnumerable<Animal>, he/she should implement it (I would say explicitly, but that has another meaning in C#). Now, would it be reasonable to produce a compilation warning about possible diamond hierarchies? Eric: your blog comments desperately needs a way to render code in <code>/<pre> tags (for inline/multiline code).  :-p

  • Anonymous
    December 07, 2007
    The problem here is not in covariance With present compiler, this code produces this compile error: 'Test.C' does not implement interface member 'System.Collections.IEnumerable.GetEnumerator()' If I have to implement GetEnumerator in anyway, I would expect that all three versions of GetEnumerator would result in enumerating the same values. So these values must be of both type Giraffe and Turtle:        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()        {            yield break ;        } It makes no sense to implement this interface more than once. foreach (var x in new C()) : how would you infer the type of Animals? the 3.0 compiler produces: foreach statement cannot operate on variables of type 'Test.C' because it implements multiple instantiations of 'System.Collections.Generic.IEnumerable<T>'; try casting to a specific interface instantiation So I expect that it should not be allowed to implement a generic interface more than once. It would be a breaking change, but it would only impact very badly written programs.

  • Anonymous
    December 07, 2007
    Yes, the problem here IS in covariance. I picked IEnumerable<T> as just one example, but you are concentrating solely on the semantics of IEnumerable.  How about if I pick some completely different interface: interface IFoo<+T> { T Get { get; } } class Bar : IFoo<Giraffe>, IFoo<Turtle> {  IFoo<Giraffe>.Get{ get { return new Giraffe(); } }  IFoo<Turtle>.Get{ get { return new Turtle(); } } } ... IFoo<Animal> f = new Bar(); Console.WriteLine(f.Get().GetType().ToString()); No IEnumerables at all. What should this do?

  • Anonymous
    December 07, 2007
    Why is implementing the same interface instantiated with two different sets of type arguments a bad programming practice?  If a Frob can be compared to a Frib or a Frub then why shouldn't class Frob : IComparable<Frib>, IComparable<Frub> be perfectly legal?

  • Anonymous
    December 14, 2007
    Speaking as a novice... The real question is about the nature of covariant interfaces: does the object bear the burden of providing the full spectrum or are an object's interface definitions pulling double-duty?  If the invariance is little more than shorthand for additional interface implementation requirements, then the class has to disambiguate IFoo<Animal> just to compile.  If the interface itself is making the guarantees, then Bar has two distinct IFoo<Animal> implementations, but neither should be directly accessible. In the former case, it's Bar's job to provide an unambiguous IFoo<Animal> implementation.  Unless the covariance guarantee is simply weak and translates to a runtime exception. In the latter case, it's the runtime's job to find the best match and detect ambiguities: IFoo<Animal> f = new Bar(); // runtime error IFoo<Mammal> m = new Bar();  // Get returns an Animal which is a Giraffe IFoo<Animal> g = new Bar() as IFoo<Giraffe>; // Get returns an Animal which is a Giraffe IFoo<Animal> t = new Bar() as IFoo<Turtle>;  // Get returns an Animal which is a Turtle

  • Anonymous
    December 19, 2007
    The comment has been removed

  • Anonymous
    May 27, 2008
    The comment has been removed

  • Anonymous
    July 26, 2008
    The comment has been removed

  • Anonymous
    November 03, 2008
    My gut says that it is a compile time error, not (as some above seem to feel?) in converting C to IEnumerable<Animal>, but in defining C to implement two interfaces related by a covariance conversion.

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