Covariance of a generic parameter type and implementation of several interfaces

If I have a common interface with a covariant type parameter, for example:

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 in the same class, for example, using an explicit implementation of the interface:

 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 and pass it to IGeneric<Derived1> or IGeneric<Derived2> , it will function 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, pressing x on IGeneric<Base> produces the following result:

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

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

Why is this allowed?

(an inspired class that implements two different IObservables ? . I tried to show my colleague that this would fail, but somehow it's not)

+45
generics c # types covariance interface
Jan 28 '13 at 12:27
source share
5 answers

The compiler cannot 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() actually a valid method on the IGeneric<Base> interface. The compiler does not track runtime b to know that there is a type that can cause ambiguity. Therefore, he stayed until runtime to decide what to do. Runtime may throw an exception, but the CLR developers apparently decided against this (which I personally consider a good solution).

In other words, let's say that instead you just wrote a method:

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

and you do not provide classes that implement IGeneric<T> in your assembly. You distribute this, and many others implement this interface only once and can simply call your method. However, someone eventually consumes your assembly and creates a DoubleDown class and passes it to your method. At what point should the compiler give an error? Of course, an already compiled and distributed assembly containing a call to GetName() cannot lead to a compiler error. It can be said that assignment from DoubleDown to IGeneric<Base> is ambiguous. but again, we could add another level of indirection to the original assembly:

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

And again, many consumers can call either CallIt or CallItOnDerived1 and be fine. But our consumer DoubleDown transition also makes a completely legal call that could not cause a compiler error when calling CallItOnDerived1 , since the conversion from DoubleDown to IGeneric<Derived1> should, of course, be in order. Thus, there is no point at which the compiler may DoubleDown error other than the DoubleDown definition, but that would preclude the possibility of doing something potentially useful without a workaround.

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

There are no warnings or errors (or runtime failures) when contravariance leads to ambiguity

Given that the likelihood of a language change to support this is almost null, I think the current behavior is fine, except that it should be outlined in the specifications so that all CLR implementations can be expected to behave the same way.

+22
Feb 09 '13 at 7:32
source share

If you 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 already understood that the result in reality changes with the order in which the interfaces for the implementation are declared . But I would say that it is simply not specified .

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

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

Here we have two questions:

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

  • Q2: Can the operator IGeneric<Base> b=x; make your signatures identical with type arguments?
    A2: No. You invoked a method using a generic covariant interface definition.

Thus, your call meets an unspecified condition. But how could this happen?
Remember, no matter what interface you specify to reference an object of type DoubleDown , it is always DoubleDown . That is, it always has two GetName methods. In fact, the interface you specify for its reference performs the selection of a contract .

Below is a portion of the captured image from a real test

enter image description here

This image shows what will be returned with GetMembers at run time. In all cases, you refer to it, IGeneric<Derived1> , IGeneric<Derived2> or IGeneric<Base> , nothing else. After the two images shown in more detail:

enter image description hereenter image description here

The images show that these two generic interfaces do not have the same name, and the other signatures / tokens make them identical.

And now you just know why.

+27
Feb 05 '13 at 17:05
source share

Question: "Why does this not lead to a compiler warning?". In VB, it does (I implemented it).

The type system does not carry enough information to provide a warning during dispersion ambiguity calls. Therefore, a warning must be issued earlier ...

  • In VB, if you declare a class C that implements both IEnumerable(Of Fish) and IEnumerable(Of Dog) , then it gives a warning that both conflict in the general case of IEnumerable(Of Animal) . This is enough to eradicate variance - the ambiguity of code written entirely in VB.

    However, this does not help if the problem class has been declared in C #. Also note that it is wise to declare such a class if no one calls the problem element on it.

  • In VB, if you are casting from such a class C to IEnumerable(Of Animal) , then it gives a broadcast warning. This is enough to eliminate variance-ambiguity , even if you imported the problem class from metadata .

    However, this is a bad place of warning because it does not work: you cannot go and change the composition. The only effective warning for people would be to go back and change the definition of the class . Also note that fully execute <personal> <personal> if no one causes a problem element on it.

  • Question:

    Why does VB emit these warnings, but C # doesn't?

    Answer:

    When I put them in VB, I was delighted with formal informatics and only wrote compilers for a couple of years, and I had the time and enthusiasm to encode them.

    Eric Lippert made them in C #. He had the wisdom and maturity to understand that coding such warnings in the compiler would require a lot of time, which could be better spent elsewhere, and was complex enough to carry a high risk. In fact, VB compilers had errors in these very warnings, which were recorded only in VS2012.

In addition, to be honest, it was impossible to come up with a warning message useful enough for people to understand. By the way

  • Question:

    How does the CLR resolve ambiguity when choosing which call?

    Answer:

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

+11
Mar 13 '13 at 19:02
source share

Holy goodness, many really good answers here to that rather difficult question. Summarizing:

  • The language specification does not clearly say what to do here.
  • This scenario usually occurs when someone tries to emulate interface covariance or contravariance; Now that C # has an interface variance, we hope that fewer people will use this template.
  • Most of the time, β€œjust pick one” is smart behavior.
  • How the CLR actually selects which implementation is used in an ambiguous covariant transformation is determined by the implementation. Basically, it scans the metadata tables and selects the first match, and C # generates the tables in the order of the source code. However, you cannot rely on this behavior; or may change without notice.

I would add only one thing, namely: the bad news is that the semantics of interface overriding do not exactly match the behavior specified in the CLI specification in scenarios where similar disagreements arise. The good news is that the actual CLR behavior when reimplementing an interface with such ambiguity is usually the behavior you want. Finding out this fact led to a vigorous debate between me, Anders and some CLI support specialists, and the end result has not changed in either the specification or the implementation. Since most C # users do not even know what the interface needs to be reissued for, we hope this will not adversely affect users. (No customer ever brought this to my attention.)

+11
Mar 13 '13 at 21:18
source share

Trying to delve into the "C # language specification" seems like the behavior is not specified (unless I got lost in my way).

7.4.4 Calling a member function

Processing the execution time of a member function call consists of the following steps, where M is a member of the function, and if M is an instance member, E is an instance expression:

[...]

o The implementation of the function member for the call is determined by

β€’ If the compile-time type E is an interface, then the function element to call is the implementation of M, provided by the run-time type of the instance referenced by E. This member of the function is determined by applying the interface matching rule (Β§13.4.4) to determine the implementation of M, provided by the runtime type of the instance referenced by E.

13.4.4 Display Interface

An interface mapping for a class or structure C finds an implementation for each member of each interface specified in the list of base class C. The implementation of a specific member of the IM interface, where I am the interface in which the member M is declared, is determined by examining each class or structure S, starting with C and repeating for each subsequent base class C until a match is found:

β€’ If S contains an declaration of an explicit implementation of an interface member that matches I and M, then this member is an IM implementation

β€’ Otherwise, if S contains a non-static public member declaration that matches M, then this member is an implementation of IM. If more than one member is found , it is not specified of which IM implementation is a member . This situation can only occur if S is a constructed type in which two members declared in the generic type have different signatures, but the type arguments make their signatures identical.

+2
Jan 28 '13 at 13:26
source share



All Articles