genericstypes参数协方差和多个接口实现
如果我有一个协变types参数的通用接口,如下所示:
interface IGeneric<out T> { string GetName(); }
如果我定义这个类层次结构:
class Base {} class Derived1 : Base{} class Derived2 : Base{}
然后,我可以使用显式的接口实现在一个类上实现接口两次,如下所示:
class DoubleDown: IGeneric<Derived1>, IGeneric<Derived2> { string IGeneric<Derived1>.GetName() { return "Derived1"; } string IGeneric<Derived2>.GetName() { return "Derived2"; } }
如果我使用(非generics的) 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
但是,将x
为IGeneric<Base>
将得到以下结果:
IGeneric<Base> b = x; Console.WriteLine(b.GetName()); //Derived1
我期望编译器发出一个错误,因为调用在两个实现之间是不明确的,但它返回了第一个声明的接口。
为什么这是允许的?
(受到一个实施两个不同IOb庇护的课程的启发?我试图向一个同事表明,这将会失败,但是不知道怎么做)
编译器不能在线上抛出错误
IGeneric<Base> b = x; Console.WriteLine(b.GetName()); //Derived1
因为编译器可以知道的没有歧义。 GetName()
实际上是接口IGeneric<Base>
上的有效方法。 编译器不会跟踪b
的运行时间,以知道存在可能导致歧义的types。 所以它由运行时决定要做什么。 运行时可能会抛出一个exception,但CLR的devise者显然决定了这一点(我个人认为这是一个很好的决定)。
换句话说,让我们说,而不是你写了这个方法:
public void CallIt(IGeneric<Base> b) { string name = b.GetName(); }
而且你不提供在你的程序IGeneric<T>
实现IGeneric<T>
类。 你发布这个和许多其他人实现这个接口只有一次,并能够调用你的方法就好了。 但是,最终有人会使用你的程序集并创buildDoubleDown
类并将其传递给你的方法。 编译器在什么时候会抛出一个错误? 包含对GetName()
的调用的已编译和分发的程序集当然不会产生编译器错误。 你可以说从DoubleDown
到IGeneric<Base>
的赋值会产生歧义。 但是我们再次可以在原始程序集中添加另一个间接程度:
public void CallItOnDerived1(IGeneric<Derived1> b) { return CallIt(b); //b will be cast to IGeneric<Base> }
再次,许多消费者可以调用CallIt
或CallItOnDerived1
并且没问题。 但是,我们的消费者传递DoubleDown
也是做一个完全合法的调用,当他们调用CallItOnDerived1
时,不会导致编译器错误,因为从DoubleDown
转换为IGeneric<Derived1>
当然是可以的。 因此,编译器可以在DoubleDown
的定义之外抛出一个错误,这是没有意义的,但是这将消除在没有解决方法的情况下做一些可能有用的事情的可能性。
其实我已经在其他地方更深入地回答了这个问题,并且如果语言可以改变,也提供了一个可能的解决scheme:
当反转导致歧义时,不会有警告或错误(或运行时失败)
考虑到语言改变的可能性几乎为零,我认为目前的行为是正确的,除了它应该在规范中进行规定,以便CLR的所有实现都将以相同的方式运行。
如果你已经testing了两个:
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"; } }
您必须已经意识到现实中的结果会随着您声明要实现的接口的顺序而改变 。 但是我会说, 这只是没有说明的 。
首先,规范(§13.4.4接口映射)说:
- 如果不止一个成员匹配,则没有指定哪个成员是IM的实现
- 只有当S是一个构造types时, 才会出现这种情况,即在genericstypes中声明的两个成员具有不同的签名 ,但types参数使它们的签名完全相同。
这里我们有两个问题需要考虑:
-
Q1:你的通用接口有不同的签名吗?
A1:是的。 它们是IGeneric<Derived2>
和IGeneric<Derived1>
。 -
Q2:可以声明
IGeneric<Base> b=x;
使他们的签名与types参数相同?
A2:不。你通过一个通用的协变接口定义来调用这个方法。
因此,您的电话符合未指定的条件。 但是,这怎么会发生呢?
请记住, 无论您指定用于引用DoubleDown
types的对象的DoubleDown
,它总是一个DoubleDown
。 也就是说,它总是有这两个GetName
方法。 您指定用来引用它的界面实际上执行合同select 。
以下是真实testing中拍摄图像的一部分
该图显示了运行时GetMembers
返回的内容。 在所有引用它的情况下, IGeneric<Derived1>
, IGeneric<Derived2>
或IGeneric<Base>
都没有什么不同。 以下两个图片更详细地显示:
图像显示,这两个通用派生接口既不具有相同的名称,也不具有其他签名/令牌使它们相同。
而现在,你只知道为什么。
神圣的善良,在这里很多很好的答案是一个相当棘手的问题。 加起来:
- 语言规范没有明确说明在这里做什么。
- 这种情况通常出现在有人试图模拟接口协方差或相反的情况下; 现在C#有接口差异,我们希望更less的人会使用这种模式。
- 大多数时候“只挑一个”是一个合理的行为。
- CLR如何select在模糊协变转换中使用哪种实现是实现定义的。 基本上,它扫描元数据表并select第一个匹配,而C#恰好按照源代码顺序发送表。 尽pipe如此,你不能依赖这种行为。 要么改变,恕不另行通知。
我只能添加一个其他的东西,那就是:坏消息是接口重新实现语义并不完全匹配在CLI规范中指定的行为,在出现这种模糊的情况下。 好消息是CLR的实际行为在重新实现这种含糊不清的接口时通常是你想要的行为。 发现这一事实导致了我,Anders和一些CLI规范维护者之间的激烈辩论,最终的结果是规范或实现都没有改变。 由于大多数C#用户甚至不知道什么接口重新开始,我们希望这不会对用户造成不利影响。 (没有客户曾经引起我的注意)
这个问题问:“为什么这不会产生编译器警告?”。 在VB中,它(我实现了它)。
types系统没有足够的信息在调用时提供关于方差模糊的警告。 所以警告必须早一点发出
-
在VB中,如果你声明一个实现了
IEnumerable(Of Fish)
和IEnumerable(Of Dog)
的类C
,那么它会给出一个警告,说明在常见情况下IEnumerable(Of Animal)
将会发生冲突。 这足以消除完全用VB编写的代码中的方差模糊性。但是,如果问题类是在C#中声明的,那么这并没有帮助。 还要注意,如果没有人调用有问题的成员,那么声明这样的类是完全合理的。
-
在VB中,如果您从这样的类
C
到IEnumerable(Of Animal)
,那么它会在演员表中发出警告。 即使从元数据导入问题类,也足以消除方差模糊性。然而,这是一个糟糕的警告位置,因为它是不可行的:你不能去改变演员阵容。 对人们唯一可行的警告是回去改变类的定义 。 另外请注意,如果没有人调用有问题的成员,那么执行这样的转换是完全合理的。
-
题:
VB如何发出这些警告,但C#不?
回答:
当我把它们放到VB中时,我对正式的计算机科学充满了热情,并且只编写了几年的编译器,我有时间和热情来编写它们。
Eric Lippert正在C#中完成他们的工作。 他有智慧和成熟的眼光,认为在编译器中编写这样的警告需要花费很多时间,可以在其他地方更好地使用,而且是非常复杂的,因此具有很高的风险。 事实上,VB编译器在这些仅在VS2012中修复的警告中存在错误。
另外,要坦率地说,不可能提出一个足够有用的警告信息,人们会理解它。 顺便,
-
题:
CLR如何在select哪一个调用时解决模糊问题?
回答:
它基于原始源代码中的inheritance语句的词汇顺序,即您声明
C
实现IEnumerable(Of Fish)
和IEnumerable(Of Dog)
的词法顺序。
试图钻研“C#语言规范”,它看起来行为没有指定(如果我没有迷路在我的方式)。
7.4.4函数成员调用
函数成员调用的运行时处理由以下步骤组成,其中M是函数成员,如果M是实例成员,则E是实例expression式:
[…]
o确定要调用的函数成员实现:
•如果E的编译时types是一个接口,那么要调用的函数成员是由E引用的实例的运行时types提供的M的实现。该函数成员是通过应用接口映射规则 13.4.4)确定由E引用的实例的运行时types提供的M的实现。
13.4.4接口映射
类或结构C的接口映射为C的基类列表中指定的每个接口的每个成员定位实现。确定特定接口成员IM(其中I是声明成员M的接口)的实现通过检查每个类或结构S,从C开始,为C的每个后续基类重复,直到find匹配:
•如果S包含与I和M匹配的显式接口成员实现的声明,则该成员是IM的实现
•否则,如果S包含与M匹配的非静态公共成员的声明,则该成员是IM的实现。如果多于一个成员匹配,则不指定哪个成员是IM的实现 。 只有当S是一个构造types时,才会出现这种情况,即在genericstypes中声明的两个成员具有不同的签名,但types参数使它们的签名完全相同。