简体   繁体   English

通用类型参数协方差和多个接口实现

[英]Generic type parameter covariance and multiple interface implementations

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:如果我使用(非泛型) DoubleDown类并将其转换为IGeneric<Derived1>IGeneric<Derived2>它会按预期运行:

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:但是,将xIGeneric<Base>会得到以下结果:

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) (受到实现两个不同 IObservables 的类的启发 。我试图向同事表明这会失败,但不知何故,它没有)

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 .但我想说它只是unspecified

First off, the specification(§13.4.4 Interface mapping) says:首先,规范(第 13.4.4 节接口映射)说:

  • If more than one member matches, it is unspecified which member is the implementation of IM如果有多个成员匹配,则未指定哪个成员是 IM 的实现
  • 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.当 S 是构造类型时才会发生这种情况,其中泛型类型中声明的两个成员具有不同的签名,但类型参数使它们的签名相同。

Here we have two questions to consider:这里我们有两个问题需要考虑:

  • Q1: Do your generic interfaces have different signatures ? Q1:你们的通用接口有不同的签名吗?
    A1: Yes. A1:是的。 They are IGeneric<Derived2> and IGeneric<Derived1> .它们是IGeneric<Derived2>IGeneric<Derived1>

  • Q2: Could the statement IGeneric<Base> b=x; Q2:可以声明IGeneric<Base> b=x; make their signatures identical with type arguments?使它们的签名与类型参数相同?
    A2: No. You invoked the method through a generic covariant interface definition. A2:不可以。您通过通用协变接口定义调用了该方法。

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 .请记住,无论您指定什么接口来引用DoubleDown类型的对象,它始终是DoubleDown That is, it always has these two GetName method.也就是说,它总是有这两个GetName方法。 The interface you specify to refer it, in fact, performs contract selection .你指定的接口引用它,实际上是执行合约选择

The following is the part of captured image from the real test以下为实测截图部分

在此处输入图片说明

This image shows what would be returned with GetMembers at runtime.此图显示了运行时GetMembers将返回的内容。 In all cases you refer it, IGeneric<Derived1> , IGeneric<Derived2> or IGeneric<Base> , are nothing different.在您提到的所有情况下, IGeneric<Derived1>IGeneric<Derived2>IGeneric<Base>都没有什么不同。 The following two image shows more details:下面两张图展示了更多细节:

在此处输入图片说明在此处输入图片说明

As the images shown, these two generic derived interfaces have neither the same name nor another signatures/tokens make them identical.如图所示,这两个通用派生接口既没有相同的名称,也没有其他签名/令牌使它们相同。

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> . GetName()实际上是接口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.编译器不会跟踪b的运行时类型以了解其中存在可能导致歧义的类型。 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).运行时可能会抛出异常,但 CLR 的设计者显然决定反对(我个人认为这是一个很好的决定)。

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.并且您没有在程序IGeneric<T>提供实现IGeneric<T>类。 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.但是,有人最终会使用您的程序集并创建DoubleDown类并将其传递到您的方法中。 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.当然,包含对GetName()调用的已编译和分发的程序集不会产生编译器错误。 You could say that the assignment from DoubleDown to IGeneric<Base> produces the ambiguity.你可以说从DoubleDownIGeneric<Base>的赋值产生了歧义。 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.再一次,许多消费者可以调用CallItCallItOnDerived1就可以了。 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.但是我们的消费者传递DoubleDown也是在进行完全合法的调用,当他们调用CallItOnDerived1时不会导致编译器错误,因为从DoubleDown转换为IGeneric<Derived1>肯定没问题。 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.因此,除了可能在DoubleDown的定义上DoubleDown ,编译器不会在任何时候抛出错误,但这将消除在没有解决方法的情况下做一些可能有用的事情的可能性。

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.鉴于语言更改以支持这一点的可能性几乎为零,我认为当前的行为是可以的,只是它应该在规范中进行布局,以便期望 CLR 的所有实现都以相同的方式运行。

The question asked, "Why doesn't this produce a compiler warning?".问题是,“为什么这不会产生编译器警告?”。 In VB, it does(I implemented it).在VB中,它确实(我实现了它)。

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) .在 VB 中,如果你声明一个C类,它同时实现了IEnumerable(Of Fish)IEnumerable(Of Dog) ,那么它会给出一个警告,说这两者在常见的IEnumerable(Of Animal)情况下会发生冲突。 This is enough to stamp out variance-ambiguity from code that's written entirely in VB.这足以从完全用 VB 编写的代码中消除差异歧义。

    However, it doesn't help if the problem class was declared in C#.但是,如果问题类是在 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.在 VB 中,如果您将C类转换为IEnumerable(Of Animal) ,则会在转换时发出警告。 This is enough to stamp out variance-ambiguity even if you imported the problem class from metadata .即使您从 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?为什么 VB 会发出这些警告而 C# 不会?

    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.当我将它们放入 VB 时,我对正式的计算机科学充满热情,并且只编写了几年编译器,我有时间和热情来编写它们。

    Eric Lippert was doing them in C#. Eric Lippert正在用 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.事实上,VB 编译器在这些警告中存在错误,这些错误仅在 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?在选择调用哪一个时,CLR 如何解决歧义?

    Answer:回答:

    It bases it on the lexical ordering of inheritance statements in the original source code, ie the lexical order in which you declared that C implements IEnumerable(Of Fish) and IEnumerable(Of Dog) .它立足它继承报表的原始源代码的词汇顺序,即词法顺序您宣布C工具IEnumerable(Of Fish)IEnumerable(Of Dog)

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.现在 C# 有接口变化,我们希望更少的人会使用这种模式。
  • 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. CLR 如何实际选择在模糊协变转换中使用哪个实现是实现定义的。 Basically, it scans the metadata tables and picks the first match, and C# happens to emit the tables in source code order.基本上,它扫描元数据表并选择第一个匹配项,而 C# 恰好按源代码顺序发出这些表。 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.我只想添加另一件事,那就是:坏消息是,在出现此类歧义的情况下,接口重新实现语义与 CLI 规范中指定的行为并不完全匹配。 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.好消息是,在重新实现具有这种歧义的接口时,CLR 的实际行为通常是您想要的行为。 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.发现这一事实引发了我、Anders 和一些 CLI 规范维护者之间的激烈辩论,最终结果是规范或实现都没有改变。 Since most C# users do not even know what interface reimplementation is to begin with, we hope that this will not adversely affect users.由于大多数 C# 用户甚至不知道从什么接口重新实现开始,我们希望这不会对用户产生不利影响。 (No customer has ever brought it to my attention.) (没有客户曾引起我的注意。)

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).试图深入研究“C# 语言规范”,它看起来没有指定行为(如果我没有迷路的话)。

7.4.4 Function member invocation 7.4.4 函数成员调用

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:函数成员调用的运行时处理包括以下步骤,其中 M 是函数成员,如果 M 是实例成员,则 E 是实例表达式:

[...] [...]

o The function member implementation to invoke is determined: o 确定要调用的函数成员实现:

• 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. • 如果 E 的编译时类型是接口,则要调用的函数成员是 E 引用的实例的运行时类型提供的 M 的实现。该函数成员通过应用接口映射规则确定(§ 13.4.4) 确定由 E 引用的实例的运行时类型提供的 M 的实现。

13.4.4 Interface mapping 13.4.4 接口映射

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 IM, 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:类或结构的接口映射 C 为 C 的基类列表中指定的每个接口的每个成员定位一个实现。特定接口成员 IM 的实现,其中 I 是声明成员 M 的接口,确定通过检查每个类或结构 S,从 C 开始并重复 C 的每个连续基类,直到找到匹配项:

• If S contains a declaration of an explicit interface member implementation that matches I and M, then this member is the implementation of IM • 如果 S 包含匹配 I 和 M 的显式接口成员实现的声明,则该成员是 IM 的实现

• Otherwise, if S contains a declaration of a non-static public member that matches M, then this member is the implementation of IM If more than one member matches, it is unspecified which member is the implementation of IM . • 否则,如果 S 包含与 M 匹配的非静态公共成员的声明,则该成员是 IM 的实现。如果有多个成员匹配,则未指定哪个成员是 IM 的实现 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.仅当 S 是构造类型时才会发生这种情况,其中泛型类型中声明的两个成员具有不同的签名,但类型参数使它们的签名相同。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM