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

SWe*_*eko 44 c# generics types interface covariance

如果我有一个带有协变类型参数的泛型接口,如下所示:

interface IGeneric<out T>
{
    string GetName();
}
Run Code Online (Sandbox Code Playgroud)

如果我定义这个类层次结构:

class Base {}
class Derived1 : Base{}
class Derived2 : Base{}
Run Code Online (Sandbox Code Playgroud)

然后我可以使用显式接口实现在单个类上实现两次接口,如下所示:

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

   string IGeneric<Derived2>.GetName()
   {
     return "Derived2";
   }  
}
Run Code Online (Sandbox Code Playgroud)

如果我使用(非泛型)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
Run Code Online (Sandbox Code Playgroud)

但是,转换xIGeneric<Base>,给出以下结果:

IGeneric<Base> b = x;
Console.WriteLine(b.GetName());   //Derived1
Run Code Online (Sandbox Code Playgroud)

我期望编译器发出错误,因为两个实现之间的调用是不明确的,但它返回了第一个声明的接口.

为什么允许这样做?

(受到实现两个不同IObservablesA类的启发我试图向同事证明这会失败,但不知何故,它没有)

Ken*_*Kin 26

如果你测试了两个:

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";
    }
}
Run Code Online (Sandbox Code Playgroud)

您必须已经意识到实际结果会随着您声明要实现的接口而改变.但我会这么说,它只是未说明.

首先,规范(§13.4.4接口映射)说:

  • 如果多个成员匹配,则未指定哪个成员是IM的实现
  • 只有当S是一个构造类型时,才会出现这种情况,其中泛型类型中声明的两个成员具有不同的签名,但类型参数使它们的签名相同.

在这里我们有两个需要考虑的问题:

  • Q1:您的通用接口是否有不同的签名
    A1:是的.他们是IGeneric<Derived2>IGeneric<Derived1>.

  • Q2:声明是否IGeneric<Base> b=x;可以使其签名与类型参数相同?
    A2:不.您通过通用协变接口定义调用了该方法.

因此,您的呼叫符合未指定的条件.但这怎么可能发生呢?
请记住,无论您指定哪个接口来引用类型对象DoubleDown,它总是一个DoubleDown.也就是说,它总是有这两种GetName方法.实际上,您指定用于引用它的接口执行合同选择.

以下是来自真实测试的捕获图像的一部分

在此输入图像描述

此图显示了GetMembers在运行时返回的内容.在所有情况下,你引用它IGeneric<Derived1>,IGeneric<Derived2>或者IGeneric<Base>,没有什么不同.以下两张图片更详细地显示:

在此输入图像描述 在此输入图像描述

图像显示,这两个通用派生接口既没有相同的名称,也没有其他签名/令牌使它们相同.

而现在,你只知道原因.

  • 有许多理由说明为什么在规范中有"严格定义的疑点和不确定性"是谨慎的; 说特定行为是"实现定义"并不一定是规范中的缺陷.有关其中一些问题的讨论,请参见http://blogs.msdn.com/b/ericlippert/archive/2012/06/18/implementation-defined-behaviour.aspx. (3认同)

jam*_*eff 24

编译器不能在行上抛出错误

IGeneric<Base> b = x;
Console.WriteLine(b.GetName());   //Derived1
Run Code Online (Sandbox Code Playgroud)

因为没有编译器可以知道的歧义. GetName()实际上是接口上的有效方法IGeneric<Base>.编译器不跟踪运行时间,b以了解其中存在可能导致歧义的类型.因此,由运行时决定要做什么.运行时可能会抛出一个异常,但CLR的设计者显然决定反对(我个人认为这是一个很好的决定).

换句话说,让我们说你只是编写了方法:

public void CallIt(IGeneric<Base> b)
{
    string name = b.GetName();
}
Run Code Online (Sandbox Code Playgroud)

并且您不提供IGeneric<T>在程序集中实现的类.你分发这个和许多其他人只实现这个接口一次,并能够很好地调用你的方法.但是,有人最终会使用您的程序集并创建DoubleDown该类并将其传递给您的方法.应该在什么时候编译器抛出错误?当然,包含调用的已编译和分布式程序集GetName()不能产生编译器错误.你可以说分配DoubleDownIGeneric<Base>产生歧义.但是我们可以再次在原始程序集中添加另一级别的间接:

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

再次,很多消费者可以打电话或者CallItCallItOnDerived1与就好了.但是,我们的消费者的传球DoubleDown也被制作完全合法呼叫,当他们打电话,可能不会导致编译器错误CallItOnDerived1从转换DoubleDownIGeneric<Derived1>肯定应该没问题.因此,没有必要编译器可以抛出除定义之外的错误DoubleDown,但是这将消除在没有解决方法的情况下做一些可能有用的事情的可能性.

我实际上在其他地方更深入地回答了这个问题,并且如果语言可以改变,也提供了一个潜在的解决方案:

当逆变导致模糊时,没有警告或错误(或运行时故障)

鉴于语言改变支持这一点的几率几乎为零,我认为当前的行为是正常的,除了它应该在规范中列出,以便预期CLR的所有实现都以相同的方式运行.


Eri*_*ert 11

圣洁的善良,这里有很多非常好的答案,这是一个非常棘手的问题.加起来:

  • 语言规范没有明确说明在这里做什么.
  • 当有人试图模仿界面协方差或逆变时,通常会出现这种情况; 既然C#有接口差异,我们希望更少的人会使用这种模式.
  • 大多数时候"只选一个"是一种合理的行为.
  • CLR如何实际选择在模糊协变转换中使用哪个实现是实现定义的.基本上,它扫描元数据表并选择第一个匹配,并且C#恰好以源代码顺序发出表.但你不能依赖这种行为; 要么改变,恕不另行通知

我只添加了另外一件事,那就是:坏消息是,在出现这些歧义的情况下,接口重新实现语义与CLI规范中指定的行为并不完全匹配.好消息是,当重新实现具有这种模糊性的界面时,CLR的实际行为通常是您想要的行为.发现这一事实导致我,Anders和一些CLI规范维护者之间激烈争论,最终结果是规范或实现都没有变化.由于大多数C#用户甚至不知道接口重新实现是什么,我们希望这不会对用户产生负面影响.(没有客户引起我的注意.)


Luc*_*hik 10

问题是,"为什么不产生编译器警告?".在VB中,它确实(我实现了它).

类型系统没有携带足够的信息来在调用关于方差模糊提供警告.所以警告必须提前发出......

  1. 在VB中,如果你声明一个类C,它实现既IEnumerable(Of Fish)IEnumerable(Of Dog),那么它会发出警告称双方将在通常情况下发生冲突IEnumerable(Of Animal).这足以消除完全用VB编写的代码中的方差模糊性.

    但是,如果在C#中声明了问题类,则无效.另请注意,如果没有人在其上调用有问题的成员,则声明这样的类是完全合理的.

  2. 在VB中,如果你从这样的类执行C转换IEnumerable(Of Animal),那么它会在演员表上发出警告.即使您从元数据导入问题类,这也足以消除方差模糊.

    然而,这是一个糟糕的警告位置,因为它不可行:你不能去改变演员阵容.对人们唯一可操作的警告是返回并更改类定义.还要注意,如果没有人在其上调用有问题的成员,那么执行这样的演员是完全合理的.

  • 题:

    VB如何发出这些警告但C#没有?

    回答:

    当我把它们放到VB中时,我对正式的计算机科学充满热情,并且只编写了几年的编译器,我有时间和热情来编写它们.

    Eric Lippert正在用C#做这些.他有智慧和成熟,认为在编译器中编写这样的警告将花费大量时间,可以更好地在其他地方使用,并且足够复杂以至于它具有高风险.事实上,VB编译器在这些警告中存在错误,这些错误仅在VS2012中得到修复.

而且,坦率地说,不可能提出足够有用的警告信息,以便人们理解它.偶然,

  • 题:

    在选择调用哪一个时,CLR如何解决歧义?

    回答:

    它基于原始源代码中的继承语句的词法排序,即您声明C实现IEnumerable(Of Fish)和的语法顺序IEnumerable(Of Dog).

  • 你是一个非常善良的卢西恩,对你自己有点不必要; 这是一个艰难的电话,我可以看到任何一方的争论.我注意到我*为C#添加了一个警告,因为一个类似的情况由于不幸的类型统一而具有实现定义的行为:http://blogs.msdn.com/b/ericlippert/archive/2006/04/ 06/570126.aspx (5认同)