使用具有协变泛型参数的显式接口实现时,出现了StackOverflowException。

3
我正在使用协变泛型参数扩展现有的IClonable接口。
public interface ICloneable<out T> : ICloneable
{
    new T Clone();
}

现在我在一个基类中实现了这个接口。
public class Base : ICloneable<Base>
{
    public string StrValue { get; set; }

    Base ICloneable<Base>.Clone()
    {
        var result = (Base)FormatterServices.GetUninitializedObject(this.GetType());
        result.StrValue = this.StrValue;
        return result;
    }

    public virtual object Clone()
    {
        return ((ICloneable<Base>)this).Clone();
    }
}

调用Clone()方法可以按预期返回一个新的基类实例,其值相同。

我创建了一个Base的派生类,再次实现了接口ICloneable<T>以返回这个新类型:

public class Sub : Base, ICloneable<Sub>
{
    public int IntValue { get; set; }

    Sub ICloneable<Sub>.Clone()
    {
        var result = (Sub)base.Clone();
        result.IntValue = this.IntValue;
        return result;
    }

    public override object Clone()
    {
        return ((ICloneable<Sub>)this).Clone();
    }
}

但是,如果我在一个Sub实例上调用Clone(),我会遇到堆栈溢出异常,因为object Base.Clone()调用了类SubSub ICloneable<Sub>.Clone()

问题在于协变的泛型类型参数。如果我移除out,所有东西都按预期工作。

问题是为什么((ICloneable<Base>)this).Clone()指向Sub.Clone()

协变和逆变只是语法糖吗?编译器是否会在派生层次结构中搜索最低可能的类型?这意味着编译器将ICloneable<Base>更改为ICloneable<Sub>

我没有找到任何官方的解释来解释这种行为。

测试代码(不包括接口和BaseSub):

var b = new Sub { StrValue = "Hello World.", IntValue = 42 };
var b2 = (Base)b.Clone();
1个回答

5
“协变和逆变”不仅仅是语法糖,编译器还会在派生层次结构中搜索最低可能的类型。协变允许您传递“更小”的、更具体的类型,而逆变则允许传递较大的类型,但需要在编译时遵守特定的编译器限制。因为参数“T”被标记为“out”,所以CLR会在运行时查找任何覆盖实现的“Clone”。编译时绑定是对“base.Clone”的“callvirt”调用,这不会改变:
.method public hidebysig newslot virtual 
instance object Clone () cil managed 
{
    // Method begins at RVA 0x2089
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: callvirt instance !0 class ICloneable`1<class Base>::Clone()
    IL_0006: ret

} // end of method Base::Clone

运行时是多态发生的地方。

移除out会调用基类,这一事实强调了这一点。


CLR将在运行时查找Clone的任何重写实现。也许我没有正确理解您的意思,但是ICloneable<T>.Clone()没有重写实现。唯一的Clone重写是非泛型的IClonable.Clone()。也许您的意思是CLR在运行时查找更具体的类型,找到它并将调用更改为更具体的类型。这没问题。在这种特殊情况下,有没有办法阻止CLR这样做? - Sebastian Schumann
1
@Vera rind 因为 ICloneable<out T> 是协变的,所以当你调用 ((ICloneable<Base>)this).Clone(); 时,对 this 的转换匹配了 ICloneable<Sub> 而不是 ICloneable<Base> - Yuval Itzchakov
是的,我明白。但最后一个问题仍然没有解决:在这种特定情况下,有没有办法防止CLR这样做? - Sebastian Schumann
@Verarind 你为什么要扩展现有的 ICloneable 而不是直接实现你的泛型接口呢? - Yuval Itzchakov
是的,因为有些代码不知道确切的类型,也需要创建克隆。我们有动态 UI,可以反射属性并创建布局。为了能够取消所有更改,我们需要一个克隆,此时我们使用 IClonable。所有其他知道类型的部分都使用 ICloneable<T> - Sebastian Schumann
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接