C# 我<;D>;重新实施I<;B>;如果我<;D>;可转换为I<;B>;通过方差转换? 接口可克隆 { T克隆(); } 类库:iClonable { 公共基克隆(){返回新基();} } 派生类:基本,可克隆 { 新的公共派生克隆(){返回新的派生克隆();} }

C# 我<;D>;重新实施I<;B>;如果我<;D>;可转换为I<;B>;通过方差转换? 接口可克隆 { T克隆(); } 类库:iClonable { 公共基克隆(){返回新基();} } 派生类:基本,可克隆 { 新的公共派生克隆(){返回新的派生克隆();} },c#,c#-4.0,covariance,contravariance,variance,C#,C# 4.0,Covariance,Contravariance,Variance,给定这些类型声明,C#规范的哪一部分解释了为什么下面代码片段的最后一行显示“True”?开发人员可以依赖这种行为吗 interface ICloneable<out T> { T Clone(); } class Base : ICloneable<Base> { public Base Clone() { return new Base(); } } class Derived : Base, ICloneable<Derived> {

给定这些类型声明,C#规范的哪一部分解释了为什么下面代码片段的最后一行显示“True”?开发人员可以依赖这种行为吗

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(); }
}
Derived=new-Derived();
基数b=d;
可克隆cb=d;
Console.WriteLine(b.Clone()是派生的);//“False”:调用Base.Clone()
Console.WriteLine(cb.Clone()是派生的);//“True”:调用派生的.Clone()

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

它只能有一个含义:方法
新公共派生克隆()
实现两个
ICloneable
ICloneable
。只有显式调用
Base.Clone()
调用隐藏方法。

我认为这是因为调用:

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
ICloneable cb=d;

如果没有差异,则
cb
只能表示
ICloneable
。但使用方差,它也可以表示可克隆的,显然,
d
的转换比
ICloneable

的转换更接近、更好。在我看来,规范的相关部分将控制两种可能的隐式引用转换中的哪一种对赋值起作用
ICloneable cb=d。从第6.1.6节“隐式引用转换”中选择的两个选项是:

  • 从任何类类型S到任何接口类型T,只要S实现T
(根据第13.4节,此处
派生的
实现
可克隆的
,因为“当类C直接实现接口时,从C派生的所有类也隐式实现接口,
基本的
直接实现
可克隆的
,所以
派生的
隐式实现它。)

  • 从任何引用类型到接口或委托类型T,如果其具有到接口或委托类型T0的隐式标识或引用转换,且T0的方差可转换(§13.1.3.2)到T
(此处,
Derived
可隐式转换为
ICloneable
,因为它直接实现,
ICloneable
可转换为
ICloneable

但是我找不到规范中处理消除隐式引用转换歧义的任何部分。

这很复杂

对b.Clone的调用显然必须调用BC。这里根本不涉及任何接口!要调用的方法完全由编译时分析决定。因此,它必须返回Base的实例。这个不太有趣

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

我们必须确定两件事来解释这种行为。第一:调用哪个“插槽”?第二:那个槽里有什么方法

派生实例必须有两个插槽,因为必须实现两种方法:
ICloneable.Clone
ICloneable.Clone
。让我们把这些插槽称为ICDC和ICBC

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

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

这似乎很奇怪。显然,插槽ICDC的内容必须是DC,但为什么插槽ICBC的内容也应该是DC?C#规范中是否有任何东西可以证明这种行为

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

ICloneable<Base> cb = d;
然后,就IFoo的方法而言,我们从D中的零开始。关于B的哪些方法映射到IFoo的方法,B必须说的任何内容都被丢弃;D可以选择与B相同的映射,也可以选择完全不同的映射。这种行为会导致一些意想不到的情况;您可以在此处阅读更多关于它们的信息:

但是:
ICloneable
的实现是
ICloneable
的重新实现吗?这一点也不清楚。IFoo的接口重新实现是IFoo的每个基本接口的重新实现,但是
ICloneable
不是
ICloneable
的基本接口

要说这是一个接口的重新实现肯定是一个延伸;该规范并不能证明这一点

这是怎么回事

这里发生的是运行时需要填充插槽。(正如我们已经说过的,slot ICDC显然必须获得方法DC。)运行时认为这是一个接口重新实现,所以它通过从派生到基的搜索来实现,并进行第一次匹配。DC是一场多亏了方差的比赛,所以它战胜了BC

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

但是,请考虑您在这里描述的确切情况。strong>可以合理地假设,对派生实例调用

ICloneable.Clone()
的人想要得到派生的返回

当我们将方差添加到C#时,我们当然测试了您在这里提到的场景,并最终发现
class B : IFoo 
{
    ...
}
class D : B, IFoo
{
    ...
}