44

If I have a generic interface with a covariant type parameter, like this:

interface IGeneric<out T>
{
    string GetName();
}

And If I define this class hierarchy:

class Base {}
class Derived1 : Base{}
class Derived2 : Base{}

Then I can implement the interface twice on a single class, like this, using explicit interface implementation:

class DoubleDown: IGeneric<Derived1>, IGeneric<Derived2>
{
   string IGeneric<Derived1>.GetName()
   {
     return "Derived1";
   }

   string IGeneric<Derived2>.GetName()
   {
     return "Derived2";
   }  
}

If I use the (non-generic)DoubleDown class and cast it to IGeneric<Derived1> or IGeneric<Derived2> it functions as expected:

var x = new DoubleDown();
IGeneric<Derived1> id1 = x;        //cast to IGeneric<Derived1>
Console.WriteLine(id1.GetName());  //Derived1
IGeneric<Derived2> id2 = x;        //cast to IGeneric<Derived2>
Console.WriteLine(id2.GetName());  //Derived2

However, casting the x to IGeneric<Base>, gives the following result:

IGeneric<Base> b = x;
Console.WriteLine(b.GetName());   //Derived1

I expected the compiler to issue an error, as the call is ambiguous between the two implementations, but it returned the first declared interface.

Why is this allowed?

(inspired by A class implementing two different IObservables?. I tried to show to a colleague that this will fail, but somehow, it didn't)

Community
  • 1
  • 1
SWeko
  • 30,434
  • 10
  • 71
  • 106
  • Regarding `Console.WriteLine(b.GetName());` the *compiler* cannot issue any error; it has a IGeneric to call getName on and that is perfectly valid call. – Miserable Variable Feb 06 '13 at 20:50
  • @MiserableVariable The compiler has more than a valid implementation - it has two of them. In other scenarios you can get an ambiguous call compile time error, in this one you don't, you get behaviour that is not specified. – SWeko Feb 07 '13 at 06:45
  • @SWeko the compiler only looks at the *static* type of `b` which is `IGeneric`, on which the `GetName` call is valid. If you are suggesting the error should be in `DoubleDown` it is not an error because there is a well-defined rule that the match is unspecified. – Miserable Variable Feb 07 '13 at 17:04
  • @MiserableVariable as I've said the other answers, this case is not covered in either of the two points of #13.4.4. – SWeko Feb 07 '13 at 19:18
  • 2
    This is really similar to a question asked earlier by me (linked by jam40jeff's answer below). Also note that Eric Lippert asked about exactly this issue in his pre-C#-4.0 blog post [Covariance and Contravariance in C#: Dealing With Ambiguity](http://blogs.msdn.com/b/ericlippert/archive/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity.aspx). He assumed `IEnumerable<>` was covariant, and made a class `C` that was both `IEnumerable` and `IEnumerable`. Then an instance of that class, by covariance, was `IEnumerable`. So, same ambiguity. – Jeppe Stig Nielsen Feb 11 '13 at 09:59

5 Answers5

27

If you have tested both of:

class DoubleDown: IGeneric<Derived1>, IGeneric<Derived2> {
    string IGeneric<Derived1>.GetName() {
        return "Derived1";
    }

    string IGeneric<Derived2>.GetName() {
        return "Derived2";
    }
}

class DoubleDown: IGeneric<Derived2>, IGeneric<Derived1> {
    string IGeneric<Derived1>.GetName() {
        return "Derived1";
    }

    string IGeneric<Derived2>.GetName() {
        return "Derived2";
    }
}

You must have realized that the results in reality, changes with the order you declaring the interfaces to implement. But I'd say it is just unspecified.

First off, the specification(§13.4.4 Interface mapping) says:

  • If more than one member matches, it is unspecified which member is the implementation of I.M.
  • This situation can only occur if S is a constructed type where the two members as declared in the generic type have different signatures, but the type arguments make their signatures identical.

Here we have two questions to consider:

  • Q1: Do your generic interfaces have different signatures?
    A1: Yes. They are IGeneric<Derived2> and IGeneric<Derived1>.

  • Q2: Could the statement IGeneric<Base> b=x; make their signatures identical with type arguments?
    A2: No. You invoked the method through a generic covariant interface definition.

Thus your call meets the unspecified condition. But how could this happen?

Remember, whatever the interface you specified to refer the object of type DoubleDown, it is always a DoubleDown. That is, it always has these two GetName method. The interface you specify to refer it, in fact, performs contract selection.

The following is the part of captured image from the real test

enter image description here

This image shows what would be returned with GetMembers at runtime. In all cases you refer it, IGeneric<Derived1>, IGeneric<Derived2> or IGeneric<Base>, are nothing different. The following two image shows more details:

enter image description here enter image description here

As the images shown, these two generic derived interfaces have neither the same name nor another signatures/tokens make them identical.

Ken Kin
  • 4,503
  • 3
  • 38
  • 76
  • Your analysis is correct, however, since we are dealing with explicit interface method implementation, it should be covered by the first point of 13.4.4. I think the second point's multiple clause deals with an entirely different scenario. IMHO, this is just a flaw in the specification. BTW, thanx for the bounty. I meant to put up one, but life got in the way :) – SWeko Feb 05 '13 at 21:14
  • What you told would mean that it should have a method named `IGeneric.GetName()` which it does not in fact. Even if it did instead of what it do, still resulting the ***actual contract*** selected(mapped) at runtime, and falls into second situation of §13.4.4 told. None of implementations would cause compilation issue, but current implementation is simpler. – Ken Kin Feb 06 '13 at 14:39
  • In point 1, the phrase used is "explicit interface member implementation that matches I and M", and `IGeneric.GetName()` does match `IGeneric.GetName()` (albeit not being the same, which is the point of the co/contra-variance). In point 2, it's said that the methods must be "a declaration of a non-static public member that matches M", which `IGeneric.GetName()` is not. – SWeko Feb 06 '13 at 14:45
  • Oh, maybe I just found why you would think so of **which `IGeneric.GetName()` is not**. If you explicitly specified a modifier except *public* with the interface or with its members and try to compile you just see that. – Ken Kin Feb 06 '13 at 15:07
  • But explicit interface method implementations are neither public nor private, and it's explicitly prohibited (in #13.4.1) to declare them as such. – SWeko Feb 06 '13 at 21:06
  • I definitely agree with @SWeko that this is a flaw in the specification; if you look at the C++ standard, these things are described in more detail. The reason here is that the precedence of interfaces used for covariance is the same as the order in which they are defined on the type - which has nothing to do with how they are bound on the method level (which can be observed by typeof(DoubleDown).GetInterfaces() and the fact that IL explicitly defines the method/interface mapping). The best resolution to handle this ambiguity is simply to describe the exact behavior in the spec. – atlaste Feb 08 '13 at 23:26
  • @KenKin well if it's important like this and not an implementation detail: yes. It means that f.ex. Mono might behave differently than .NET in this particular case (haven't tested that btw) – atlaste Feb 09 '13 at 00:24
  • @Stefan de Bruijn: I can tell the fact why, but ***specification flawn*** is something out of range that I can tell. – Ken Kin Feb 10 '13 at 01:40
  • 3
    There are a number of reasons why it is prudent to have "rigidly defined areas of doubt and uncertainty" in the specification; saying that a particular behaviour is "implementation defined" is not necessarily a flaw in the specification. See http://blogs.msdn.com/b/ericlippert/archive/2012/06/18/implementation-defined-behaviour.aspx for a discussion of some of these issues. – Eric Lippert Mar 13 '13 at 21:51
26

The compiler can't throw an error on the line

IGeneric<Base> b = x;
Console.WriteLine(b.GetName());   //Derived1

because there is no ambiguity that the compiler can know about. GetName() is in fact a valid method on interface IGeneric<Base>. The compiler doesn't track the runtime type of b to know that there is a type in there which could cause an ambiguity. So it's left up to the runtime to decide what to do. The runtime could throw an exception, but the designers of the CLR apparently decided against that (which I personally think was a good decision).

To put it another way, let's say that instead you simply had written the method:

public void CallIt(IGeneric<Base> b)
{
    string name = b.GetName();
}

and you provide no classes implementing IGeneric<T> in your assembly. You distribute this and many others implement this interface only once and are able to call your method just fine. However, someone eventually consumes your assembly and creates the DoubleDown class and passes it into your method. At what point should the compiler throw an error? Surely the already compiled and distributed assembly containing the call to GetName() can't produce a compiler error. You could say that the assignment from DoubleDown to IGeneric<Base> produces the ambiguity. but once again we could add another level of indirection into the original assembly:

public void CallItOnDerived1(IGeneric<Derived1> b)
{
    return CallIt(b); //b will be cast to IGeneric<Base>
}

Once again, many consumers could call either CallIt or CallItOnDerived1 and be just fine. But our consumer passing DoubleDown also is making a perfectly legal call that could not cause a compiler error when they call CallItOnDerived1 as converting from DoubleDown to IGeneric<Derived1> should certainly be OK. Thus, there is no point at which the compiler can throw an error other than possibly on the definition of DoubleDown, but this would eliminate the possibility of doing something potentially useful with no workaround.

I have actually answered this question more in depth elsewhere, and also provided a potential solution if the language could be changed:

No warning or error (or runtime failure) when contravariance leads to ambiguity

Given that the chance of the language changing to support this is virtually zero, I think that the current behavior is alright, except that it should be laid out in the specifications so that all implementations of the CLR would be expected to behave the same way.

jam40jeff
  • 2,576
  • 16
  • 14
  • 1
    ***Nice answer!*** I've upvoted. You provided a much more deeply assuming to explain the fact. The comments between SWeko and me of my answer, I replied with *current implementation is simpler*. – Ken Kin Feb 10 '13 at 03:15
  • "At what point should the compiler throw an error?" Compiler should throw an error when you implicitly convert `DoubleDown` to `IGeneric` since it's ambiguous (the compiler doesn't know which of the two interfaces to use). – Ark-kun Feb 10 '13 at 20:08
  • @Ark-kun Even if the compiler would throw in error in that case, there is nowhere in the second case (where `DoubleDown` is first assigned to `IGeneric` and then later `IGeneric` is assigned to `IGeneric`) that the compiler can throw an error. – jam40jeff Feb 11 '13 at 04:18
  • Thanks. Also, I'd like to point out that the link near the end of my answer actually leads to another answer which goes more in depth. – jam40jeff Feb 13 '13 at 19:04
  • @jam40jeff The second case is fine. You explicitly chose the `IGeneric` implementation when you converted `DoubleDown` to `IGeneric`. Nothing ambiguous here. – Ark-kun Feb 13 '13 at 22:30
  • 1
    @Ark-kun You're looking at things within the context of one method (and even then the compiler doesn't track the runtime type of what's in a variable. By the time the variable is of type `IGeneric` you haven't "chosen" which implementation to use. – jam40jeff Feb 13 '13 at 23:22
  • Actually, the runtime will choose the same implementation even if you casted to `IGeneric` first and then to `IGeneric` (try it for yourself, it exposes how troublesome this ambiguity really is) because at the point the call is made, all the compiler knows is that it is calling the method on an `IGeneric`. It doesn't keep an "assigment history" or track the runtime type of the variable, so it knows nothing further than that. It isn't until runtime that the CLR realizes it actually has a `DoubleDown` to deal with and must make a choice on which call to choose. – jam40jeff Feb 13 '13 at 23:23
  • @jam40jeff Yes, I forgot about the runtime and types. That's bad. I wish C# would at least support the implementation method renaming like VB.net does. (There are 3 different interface methods implementation types which have different rules and produce different definitions: C# implicit, C# explicit and VB.Net) – Ark-kun Feb 13 '13 at 23:59
  • +1 good discussion guys - short of something like `IGeneric, IGeneric default` - that's a good call for a 'default'. Or I personally wouldn't compile the class - everything else is not a solution imo. – NSGaga-mostly-inactive Mar 30 '13 at 01:37
  • +1, but more illustrative would have been if `CallItOnDerived1` were `CallItOnDerived2` because the object’s static type changes to `IGeneric` on which GetName() returns unsurprisingly "Derived2". In total, an up-cast can change what a seemingly overridden function does. – Quirin F. Schroll Sep 15 '22 at 15:24
11

The question asked, "Why doesn't this produce a compiler warning?". In VB, it does(I implemented it).

The type system doesn't carry enough information to provide a warning at time of invocation about variance ambiguity. So the warning has to be emitted earlier ...

  1. In VB, if you declare a class C which implements both IEnumerable(Of Fish) and IEnumerable(Of Dog), then it gives a warning saying that the two will conflict in the common case IEnumerable(Of Animal). This is enough to stamp out variance-ambiguity from code that's written entirely in VB.

    However, it doesn't help if the problem class was declared in C#. Also note that it's completely reasonable to declare such a class if no one invokes a problematic member on it.

  2. In VB, if you perform a cast from such a class C into IEnumerable(Of Animal), then it gives a warning on the cast. This is enough to stamp out variance-ambiguity even if you imported the problem class from metadata.

    However, it's a poor warning location because it's not actionable: you can't go and change the cast. The only actionable warning to people would be to go back and change the class definition. Also note that it's completely reasonable to perform such a cast if no one invokes a problematic member on it.

  • Question:

    How come VB emits these warnings but C# doesn't?

    Answer:

    When I put them into VB, I was enthusiastic about formal computer science, and had only been writing compilers for a couple of years, and I had the time and enthusiasm to code them up.

    Eric Lippert was doing them in C#. He had the wisdom and maturity to see that coding up such warnings in the compiler would take a lot of time that could be better spent elsewhere, and was sufficiently complex that it carried high risk. Indeed the VB compilers had bugs in these very warnings that were only fixed in VS2012.

Also, to be frank, it was impossible to come up with a warning message useful enough that people would understand it. Incidentally,

  • Question:

    How does the CLR resolve the ambiguity when chosing which one to invoke?

    Answer:

    It bases it on the lexical ordering of inheritance statements in the original source code, i.e. the lexical order in which you declared that C implements IEnumerable(Of Fish) and IEnumerable(Of Dog).

Community
  • 1
  • 1
Lucian Wischik
  • 2,160
  • 1
  • 20
  • 25
  • 6
    You are very kind Lucian and a little unnecessarily hard on yourself; this was a tough call and I can see the arguments on either side. And I note that I *did* add a warning to C# for a similar case which has implementation-defined behaviour due to an unfortunate type unification: http://blogs.msdn.com/b/ericlippert/archive/2006/04/06/570126.aspx – Eric Lippert Mar 13 '13 at 21:05
11

Holy goodness, lots of really good answers here to what is quite a tricky question. Summing up:

  • The language specification does not clearly say what to do here.
  • This scenario usually arises when someone is attempting to emulate interface covariance or contravariance; now that C# has interface variance we hope that less people will use this pattern.
  • Most of the time "just pick one" is a reasonable behaviour.
  • How the CLR actually chooses which implementation is used in an ambiguous covariant conversion is implementation-defined. Basically, it scans the metadata tables and picks the first match, and C# happens to emit the tables in source code order. You can't rely on this behaviour though; either can change without notice.

I'd only add one other thing, and that is: the bad news is that interface reimplementation semantics do not exactly match the behaviour specified in the CLI specification in scenarios where these sorts of ambiguities arise. The good news is that the actual behaviour of the CLR when re-implementing an interface with this kind of ambiguity is generally the behaviour that you'd want. Discovering this fact led to a spirited debate between me, Anders and some of the CLI spec maintainers and the end result was no change to either the spec or the implementation. Since most C# users do not even know what interface reimplementation is to begin with, we hope that this will not adversely affect users. (No customer has ever brought it to my attention.)

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Would the described situation apply if class `Foo` implements IEnumerable`, and class `DerivedFoo` implements `IEnumerable`? Would there be any practical means via which one could avoid creating ambiguous bindings while allowing a `DerivedFoo` to be passed to code that requires an enumerable of things that are not only a kind of `Bar`, but more specifically a kind of `DerivedBar`? – supercat Mar 13 '13 at 22:14
2

Trying to delve into the "C# language specifications", it looks that the behaviour is not specified (if I did not get lost in my way).

7.4.4 Function member invocation

The run-time processing of a function member invocation consists of the following steps, where M is the function member and, if M is an instance member, E is the instance expression:

[...]

o The function member implementation to invoke is determined:

• If the compile-time type of E is an interface, the function member to invoke is the implementation of M provided by the run-time type of the instance referenced by E. This function member is determined by applying the interface mapping rules (§13.4.4) to determine the implementation of M provided by the run-time type of the instance referenced by E.

13.4.4 Interface mapping

Interface mapping for a class or struct C locates an implementation for each member of each interface specified in the base class list of C. The implementation of a particular interface member I.M, where I is the interface in which the member M is declared, is determined by examining each class or struct S, starting with C and repeating for each successive base class of C, until a match is located:

• If S contains a declaration of an explicit interface member implementation that matches I and M, then this member is the implementation of I.M.

• Otherwise, if S contains a declaration of a non-static public member that matches M, then this member is the implementation of I.M. If more than one member matches, it is unspecified which member is the implementation of I.M. This situation can only occur if S is a constructed type where the two members as declared in the generic type have different signatures, but the type arguments make their signatures identical.

Community
  • 1
  • 1
Teudimundo
  • 2,610
  • 20
  • 28
  • 2
    I don't think this is it. Interface mapping is about deciding which method in the class ends up implementing each method declared in the interfaces the class implements, not to mention that the members in question here are not public (the interfaces are implemented explicitly). – Jon Jan 28 '13 at 13:55
  • Thanks Jon. I edited the answer, the method invocation resolution uses the same mechanism, checking the run-time type. – Teudimundo Jan 28 '13 at 14:15
  • 1
    That other quote seems relevant here. But I think the behavior is actually explained by the *first* bullet: the base interfaces of `DoubleDown` are searched in turn until a method that maps to `IGeneric.GetName()` is found. So the method call maps to `IGeneric.GetName()` because that interface appears first in the inherited interfaces list. – Jon Jan 28 '13 at 14:20
  • 2
    It's true that the first refer to explicit methods. But here it says that the mapping iterates over the base classes of C, I would not expect interfaces are involved as well. I'm anyway less sure that the second point is relevant in this case. – Teudimundo Jan 28 '13 at 14:37
  • The second point cannot apply in this scenario, as the EIMI's are explicitly excluded in the first point. In this case we actually have multiple matches on the first point, so the second point is not even considered. – SWeko Jan 28 '13 at 14:42
  • Mads and I (and I think Lucian, who has also chimed in on this question!) worked up that second bullet point in the specification to deal with the situation I describe here: http://blogs.msdn.com/b/ericlippert/archive/2006/04/06/570126.aspx – Eric Lippert Mar 13 '13 at 21:08