如果我<d>可通过方差转换转换为I <b>,我是否<d>重新实施I <b> </b> </d> </b> </d>

时间:2012-01-03 03:30:58

标签: c# c#-4.0 covariance contravariance variance

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(); }
}

鉴于这些类型声明,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

请注意,如果T中的ICloneable类型参数声明为out,则两行都会打印“False”。

4 个答案:

答案 0 :(得分: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
{
    ...
}

然后就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>的{​​{1}} 重新实施的实现?它根本不应该是清楚的。 IFoo的接口重新实现是对IFoo的每个基本接口的重新实现,但ICloneable<Base>不是ICloneable<Base>基接口

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

那么这里发生了什么?

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

现在您可能会问在CLI规范中指定 行为的位置,答案是“无处”。事实上,情况比这更糟糕;仔细阅读CLI规范实际上表明指定了相反的行为。从技术上讲,CLR在这里不符合自己的规范。

但是,请考虑您在此处描述的确切情况。 可以合理地假设某人在Derived实例上调用ICloneable<Derived>想要将Derived退出!

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

所以,总结一下:

  • 事实上,CLR执行从派生到基础的第一次匹配匹配,就好像这是一个重新实现的接口。
  • De jure ,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#中确实是奇怪的。

答案 1 :(得分:4)

它只能有一个含义:方法new public Derived Clone()实现 ICloneable<Base>ICloneable<Derived>。只有对Base.Clone()的显式调用才会调用隐藏方法。

答案 2 :(得分:0)

我认为这是因为致电:

ICloneable<Base> cb = d;

没有差异,cb只能代表ICloneable<Base>。但是对于方差,它也可以表示ICloneable<Derived>,这显然比d更接近和更好,而不是转换为ICloneable<Base>

答案 3 :(得分:0)

在我看来,规范的相关部分将是控制两个可能的隐式引用转换中的哪一个对于赋值ICloneable<Base> cb = d;起作用的部分。从第6.1.6节“隐式参考转换”中选择的两个选项是:

  • 从任何类型S到任何接口类型T,只要S实现T。

(这里Derived实现ICloneable<Base>,根据13.4节,因为“当C类直接实现接口时,所有从C派生的类也会隐式实现接口,”和{{1}直接实现Base,因此ICloneable<Base>隐式实现它。)

  • 从任何引用类型到接口或委托类型T,如果它具有隐式标识或引用转换为接口或委托类型T0和T0是方差可转换(第13.1.3.2节)到T。

(此处,Derived可隐式转换为Derived,因为它直接实现,ICloneable<Derived>是方差转换为ICloneable<Derived>。)

但是我找不到规范中涉及消除隐含引用转换的任何部分。