Generic type parameter covariance and multiple interface implementations

后端 未结 5 1228
名媛妹妹
名媛妹妹 2020-12-12 22:18

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

interface IGeneric
{
    string GetName();
}

And If

相关标签:
5条回答
  • 2020-12-12 22:46

    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).

    0 讨论(0)
  • 2020-12-12 22:49

    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.

    0 讨论(0)
  • 2020-12-12 22:52

    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.

    0 讨论(0)
  • 2020-12-12 22:53

    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.)

    0 讨论(0)
  • 2020-12-12 22:55

    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.

    0 讨论(0)
提交回复
热议问题