4 回答
情况很复杂。
对 b.Clone 的调用显然必须调用 BC。这里根本不涉及任何接口!调用的方法完全由编译时分析决定。因此它必须返回一个 Base 的实例。这个不是很有趣。
相比之下,对 cb.Clone 的调用非常有趣。
我们必须建立两件事来解释这种行为。第一:调用哪个“槽”?第二:那个槽里有什么方法?
Derived 的实例必须有两个插槽,因为必须实现两个方法:ICloneable<Derived>.Clone
和ICloneable<Base>.Clone
. 我们将这些插槽称为 ICDC 和 ICBC。
显然 cb.Clone 调用的 slot 必须是 ICBC slot;编译器没有理由知道插槽 ICDC 甚至存在于 cb 上,其类型为ICloneable<Base>
.
那个槽里有什么方法?有两种方法,Base.Clone 和 Derived.Clone。我们称它们为 BC 和 DC。正如您所发现的,Derived 实例上该插槽的内容是 DC。
这似乎很奇怪。明明slot ICDC的内容一定是DC,为什么slot 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>
的实现吗?目前还不清楚它应该是。IFoo 的接口重新实现是对 IFoo 的每个基接口的重新实现,但不是!ICloneable<Base>
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 的“第一次适合”实现中暴露不合理的、依赖于实现的行为的另一个示例,请参见:
有关接口方法的非变体通用统一在 CLR 的“第一次适合”实现中暴露出不合理的、依赖于实现的行为的示例,请参见:
https://ericlippert.com/2006/04/05/odious-ambiguous-overloads-part-one/ https://ericlippert.com/2006/04/06/odious-ambiguous-overloads-part-two/
在这种情况下,您实际上可以通过重新排序程序的文本来改变程序的行为,这在 C# 中确实很奇怪。
它只能有一个含义:该方法同时new public Derived Clone()
实现和 。只有显式调用才能调用隐藏方法。 ICloneable<Base>
ICloneable<Derived>
Base.Clone()
在我看来,规范的相关部分将是控制两个可能的隐式引用转换中的哪一个对赋值起作用ICloneable<Base> cb = d;
。取自第 6.1.6 节“隐式引用转换”的两个选择是:
- 从任何类类型 S 到任何接口类型 T,只要 S 实现了 T。
(这里Derived
implements ICloneable<Base>
,根据 13.4 节,因为“当一个类 C 直接实现一个接口时,所有从 C 派生的类也隐式实现该接口”,并且Base
直接实现了ICloneable<Base>
,所以Derived
隐式实现它。)
- 从任何引用类型到接口或委托类型 T,如果它具有到接口或委托类型 T0 的隐式标识或引用转换,并且 T0 可方差转换(第 13.1.3.2 节)到 T。
(在这里,Derived
可以隐式转换为,ICloneable<Derived>
因为它直接实现它,并且ICloneable<Derived>
可以方差转换为ICloneable<Base>
.)
但是我找不到规范中处理消除隐式引用转换歧义的任何部分。
我认为这是因为调用:
ICloneable<Base> cb = d;
没有方差,则cb
只能表示ICloneable<Base>
. 但有了方差,它也可以表示ICloneable<Derived>
,这显然d
比 cast to更接近和更好ICloneable<Base>
。