如果我<D>通过方差转换可以转换为I <B>,我是否<D>重新实现I <B>?

Mic*_*Liu 20 c# covariance contravariance variance c#-4.0

interface ICloneable<out T>
{
    T Clone();
}

class Base : ICloneable<Base>
{
    public Base Clone() { return new Base(); }
}

class Derived : Base, ICloneable<Derived>
{
    new public Derived Clone() { return new Derived(); }
}
Run Code Online (Sandbox Code Playgroud)

鉴于这些类型声明,C#规范的哪一部分解释了为什么以下代码片段的最后一行打印"True"?开发人员可以依赖这种行为吗?

Derived d = new Derived();
Base b = d;
ICloneable<Base> cb = d;
Console.WriteLine(b.Clone() is Derived); // "False": Base.Clone() is called
Console.WriteLine(cb.Clone() is Derived); // "True": Derived.Clone() is called
Run Code Online (Sandbox Code Playgroud)

需要注意的是,如果T在类型参数ICloneable没有声明out,则这两条线将打印"假".

Eri*_*ert 13

情况很复杂.

对b.Clone的调用显然必须调用BC.这里根本没有涉及的界面!调用方法完全由编译时分析决定.因此它必须返回Base的实例.这个不是很有趣.

相反,对cb.Clone的调用非常有趣.

我们必须建立两件事来解释行为.第一:调用哪个"槽"?第二:那个插槽里有什么方法?

Derived的实例必须有两个插槽,因为必须实现两种方法:ICloneable<Derived>.CloneICloneable<Base>.Clone.我们称那些插槽为ICDC和ICBC.

很明显,cb.Clone调用的插槽必须是ICBC插槽; 编译器没有理由知道插槽ICDC甚至存在于类型的cb上ICloneable<Base>.

那个插槽里有什么方法?有两种方法,Base.Clone和Derived.Clone.我们称之为BC和DC.正如您所发现的,Derived实例上该插槽的内容是DC.

这看起来很奇怪.显然,插槽ICDC的内容必须是DC,但为什么插槽ICBC的内容也应该是DC?C#规范中有什么可以证明这种行为是正确的吗?

我们得到的最接近的是第13.4.6节,它是关于"接口重新实现"的.简而言之,当你说:

class B : IFoo 
{
    ...
}
class D : B, IFoo
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

那么就IFoo的方法而言,我们从头开始在D中.B必须说明关于哪些B方法映射到IFoo方法的东西被丢弃了; D可能选择与B相同的映射,或者它可能选择完全不同的映射.这种行为可能导致一些意料之外的情况; 你可以在这里阅读更多相关信息:

http://blogs.msdn.com/b/ericlippert/archive/2011/12/08/so-many-interfaces-part-two.aspx

但是:是的实现ICloneable<Derived>一个重新实现ICloneable<Base>?它根本不应该是清楚的.接口重新实现的IFoo的是重新实现每一个的基本接口 IFoo的,但ICloneable<Base>不是基本接口ICloneable<Derived>!

要说这是一个界面重新实现肯定会是一个延伸; 规范并不合理.

那么这里发生了什么?

这里发生的是运行时需要填写插槽ICBC.(正如我们已经说过的,插槽ICDC显然必须得到方法DC.)运行时认为这一个接口重新实现,所以它通过从Derived搜索到Base来完成,并进行首次匹配.由于差异,DC是匹配,所以它胜过BC.

现在你可能会问,其中行为是在CLI规范规定,得到的答复是"无处".事实上,情况比这更糟糕; 仔细阅读CLI规范显示事实上指定了相反的行为.从技术上讲,CLR在这里不符合自己的规范.

但是,请考虑您在此处描述的确切情况.可以合理地假设某个调用ICloneable<Base>.Clone()Derived实例的人想让Derived退出!

当我们向C#添加方差时,我们当然测试了你在这里提到的情况,并最终发现这种行为既不合理也不可取.然后,与CLI规范的管理员进行了一段时间的谈判,以确定我们是否应该编辑规范,以便规范证明这种理想的行为是合理的.我不记得那次谈判的结果是什么; 我没有亲自参与其中.

所以,总结一下:

  • 事实上,CLR执行从派生到基础的第一次拟合匹配,就好像这是一个接口重新实现.
  • 在法律上,C#规范或CLI规范都不能证明这一点.
  • 我们不能在不破坏人的情况下改变实施.
  • 实现在方差转换下统一的接口是危险和混乱的; 尽量避免它.

有关变体接口统一在CLR的"首次适合"实现中暴露出不合理的,依赖于实现的行为的另一个示例,请参阅:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity.aspx

对于一个示例,其中接口方法的非变体通用统一在CLR的"第一次适合"实现中暴露了一个不合理的,依赖于实现的行为,请参阅:

http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx

在这种情况下,您实际上可以通过重新排序程序的文本来改变程序行为,这在C#中确实很奇怪.