自反类型参数约束:X<T> where T : X<T> - 有更简单的替代方案吗?

19

我会经常通过添加自引用(“反身”)类型参数约束来使简单接口变得更加复杂。例如,我可能会将这个转换成:

interface ICloneable
{
    ICloneable Clone();
}

class Sheep : ICloneable
{
    ICloneable Clone() { … }
} //^^^^^^^^^^

Sheep dolly = new Sheep().Clone() as Sheep;
                                //^^^^^^^^

转化为:

interface ICloneable<TImpl> where TImpl : ICloneable<TImpl>
{
    TImpl Clone();
}

class Sheep : ICloneable<Sheep>
{
    Sheep Clone() { … }
} //^^^^^

Sheep dolly = new Sheep().Clone();

主要优势:实现类型(例如Sheep)现在可以引用自身而不是其基础类型,减少了需要进行类型转换的需要(正如代码的最后一行所示)。

虽然这很好,但我也注意到这些类型参数约束并不直观,并且在更复杂的情况下很容易变得难以理解。*)

问题:有人知道另一个C#代码模式,可以以更易于理解的方式实现相同或类似的效果吗?


*) 这个代码模式可能不直观,难以理解,例如:

  • 声明 X<T> where T : X<T> 看起来是递归的,人们可能会想知道为什么编译器不会陷入无限循环, 推理时会说:"如果 T 是一个 X<T>,那么 X<T> 实际上是一个 X<X<…<T>…>>。" (但约束显然不会像这样解决。)

  • 对于实现者来说,可能不明确应该在 TImpl 的位置指定哪种类型。(约束将最终处理这个问题。)

  • 一旦你添加了更多的类型参数和各种泛型接口之间的子类型关系,事情很快就变得难以管理。


3
你会很高兴知道这种情况已经很普遍了,有一个专门的术语来描述它:被称为“奇异递归模板模式”(简称CRTP)。 - Cameron
1
...这与约束无关(因为标准的C++模板根本没有它们)。 - Krizz
2
@pst,是的,因为没有约束条件,一个人可以将Sheep实现为class Sheep:ICloneable <Dog> {public Dog Clone(){...}},这可能不是一个人最初在ICloneable接口中所想的。 - stakx - no longer contributing
2
上周在stackoverflow上有一个相关的问题:为什么基类说明的含义不能递归地依赖于自身? - Tim Schmelter
1
@pst,结果证明你是正确的!ICloneable<T>就足够了。正如下面的答案所示,额外的约束只会确保类型参数实现了ICloneable<>接口,但不一定是相同的类型。 - stakx - no longer contributing
显示剩余3条评论
2个回答

19
主要优势:实现类型现在可以引用自身而不是其基本类型,减少了需要进行类型转换的需要。
虽然看起来通过类型约束引用自身会强制实现类型执行相同操作,但实际上并非如此。人们使用这种模式来尝试表达形式为“重写此方法必须返回覆盖类的类型”的模式,但实际上这并不是类型系统所表达或强制执行的限制。我在这里举个例子:

https://ericlippert.com/2011/02/02/curiouser-and-curiouser/

虽然这很好,但我也注意到这些类型参数约束不直观,并且倾向于在更复杂的情况下变得非常难以理解。是的,我尽量避免使用这种模式。很难进行推理。
有没有人知道另一种C#代码模式可以实现相同的效果或类似的东西,但更容易理解呢?不,在C#中没有。如果你对这种事情感兴趣,你可以考虑查看Haskell类型系统;Haskell的“高级类型”可以表示那些类型模式。
声明X where T : X似乎是递归的,人们可能会想知道为什么编译器不会陷入无限循环,推理:“如果T是X,那么X实际上是一个X…>>”。
编译器在推理这种简单关系时不会陷入无限循环。然而,带有逆变的通用类型的名义子类型判断通常是不可判定的。有一些方法可以迫使编译器进入无限回归状态,而 C# 编译器在踏上无限之旅之前并未检测到这些问题并加以防止。(但我希望在 Roslyn 编译器中添加对此的检测,我们将拭目以待。)如感兴趣,请查看我的文章。您还需要阅读链接的论文。

https://ericlippert.com/2008/05/07/covariance-and-contravariance-part-11-to-infinity-but-not-beyond/


感谢您详细的回答。您的博客文章一针见血。此外,感谢您指出当涉及到无限的事物时,C#编译器比博格集体更聪明。 :) - stakx - no longer contributing

7
不幸的是,没有一种完全防止这种情况发生的方法,而没有类型限制的通用ICloneable已经足够。你的约束只限制了可能的参数为实现它本身的类,这并不意味着它们是当前正在实现的类。
换句话说,如果一个Cow实现了ICloneable,你仍然可以轻松地让Sheep实现ICloneable。
我建议只使用没有约束的ICloneable有两个原因:
1. 我严重怀疑你会错误地使用错误的类型参数。 2. 接口意味着是其他代码部分的契约,而不是用于自动化编码。如果代码的某个部分期望ICloneable并且你传递了一个能够做到这一点的Sheep,从那里来看似乎非常合理。

1
+1 "[interface X<T> where T:X<T>{}] 只限制可能的参数为实现它的类,这并不意味着它们是当前正在实现的类。" 非常简洁,出色! - Glenn Slayden
这不太与问题相关,但我发现“克隆”概念过于具体。也许有技术情况需要对象的字面“克隆”,但我基本上从未遇到过。如果我想要一个对象的副本,通常会有一个特定的用例以及一个与“克隆”不同的特定复制方式。此外,最近我使用ISpawn <T> ,它并不意味着T必须与实现类型相关,只是表示可以从中获取Ts的某些东西。 - Dave Cousineau

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