我经常通过向简单的接口添加自引用(“自反”)类型参数约束来使简单的接口变得更加复杂。例如,我可以将其转换
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 : X
似乎是递归的,人们可能想知道为什么编译器不会卡住在无限循环中,推理,“如果 T
是 X
,则 X
真的是X...>>
。”(但约束显然不会像这样得到解决。)
< /里>
对于实现者来说,应该指定什么类型来代替 TImpl
可能并不明显。 (约束最终会解决这个问题。)
一旦添加更多类型参数以及各种泛型接口之间的子类型关系,事情很快就会变得难以管理。
Every so often I am making a simple interface more complicated by adding a self-referencing ("reflexive") type parameter constraint to it. For example, I might turn this:
interface ICloneable
{
ICloneable Clone();
}
class Sheep : ICloneable
{
ICloneable Clone() { … }
} //^^^^^^^^^^
Sheep dolly = new Sheep().Clone() as Sheep;
//^^^^^^^^
into:
interface ICloneable<TImpl> where TImpl : ICloneable<TImpl>
{
TImpl Clone();
}
class Sheep : ICloneable<Sheep>
{
Sheep Clone() { … }
} //^^^^^
Sheep dolly = new Sheep().Clone();
Main advantage: An implementing type (such as Sheep
) can now refer to itself instead of its base type, reducing the need for type-casting (as demonstrated by the last line of code).
While this is very nice, I've also noticed that these type parameter constraints are not intuitive and have the tendency to become really difficult to comprehend in more complex scenarios.*)
Question: Does anyone know of another C# code pattern that achieves the same effect or something similar, but in an easier-to-grasp fashion?
*) This code pattern can be unintuitive and hard to understand e.g. in these ways:
-
The declaration X<T> where T : X<T>
appears to be recursive, and one might wonder why the compiler doesn't get stuck in an infinite loop, reasoning, "If T
is an X<T>
, then X<T>
is really an X<X<…<T>…>>
." (But constraints obviously don't get resolved like that.)
-
For implementers, it might not be obvious what type should be specified in place of TImpl
. (The constraint will eventually take care of that.)
-
Once you add more type parameters and subtyping relationships between various generic interfaces to the mix, things get unmanageable fairly quickly.
发布评论
评论(2)
虽然看起来似乎是引用自身的类型约束,但它强制实现类型执行以下操作一样的,实际上不是这样的。人们使用此模式来尝试表达“此方法的重写必须返回重写类的类型”形式的模式,但这实际上并不是类型系统表达或强制执行的约束。我在这里举一个例子:
https://ericlippert.com/2011/02 /02/好奇和好奇/
。我尽量避免这种模式。很难推理。
不是在 C# 中,不是。如果您对这类事情感兴趣,您可以考虑查看 Haskell 类型系统; Haskell 的“高级类型”可以表示这些类型的类型模式。
在推理此类简单关系时,编译器永远不会陷入无限循环。然而,具有逆变性的泛型类型的名义子类型通常是不可判定的。有一些方法可以强制编译器陷入无限倒退,而 C# 编译器在踏上无限倒退之前不会检测到这些情况并阻止它们。 (然而,我希望在 Roslyn 编译器中添加对此的检测,但我们会看到的。)
如果您对此感兴趣,请参阅我关于该主题的文章。您还需要阅读链接到的论文。
https://ericlippert.com/2008/05/07/covariance-and-contravariance-part-11-to-infinity-but-not-beyond/
Though it might seem like by the type constraint referring to itself it forces the implementing type to do the same, that's actually not what it does. People use this pattern to try to express patterns of the form "an override of this method must return the type of the overriding class", but that's not actually the constraint expressed or enforced by the type system. I give an example here:
https://ericlippert.com/2011/02/02/curiouser-and-curiouser/
Yep. I try to avoid this pattern. It's hard to reason about.
Not in C#, no. You might consider looking at the Haskell type system if this sort of thing interests you; Haskell's "higher types" can represent those sorts of type patterns.
The compiler does not ever get into infinite loops when reasoning about such simple relationships. However, nominal subtyping of generic types with contravariance is in general undeciable. There are ways to force the compiler into infinite regresses, and the C# compiler does not detect these and prevent them before embarking on the infinite journey. (Yet. I am hoping to add detection for this in the Roslyn compiler but we'll see.)
See my article on the subject if this interests you. You'll want to read the linked-to paper as well.
https://ericlippert.com/2008/05/07/covariance-and-contravariance-part-11-to-infinity-but-not-beyond/
不幸的是,没有办法完全防止这种情况,并且没有类型约束的通用
ICloneable
就足够了。您的约束仅将可能的参数限制为本身实现它的类,这并不意味着它们是当前正在实现的类。换句话说,如果
Cow
实现了ICloneable
,您仍然可以轻松地让Sheep
实现ICloneable
代码>.我会简单地使用
ICloneable
而没有任何限制,原因有两个:我严重怀疑您是否会犯下使用错误类型参数的错误。
接口旨在成为代码其他部分的契约,而不是用于在自动驾驶仪上进行编码。如果代码的一部分需要
ICloneable
并且您传递了一个可以做到这一点的Sheep
,那么从那时起它似乎完全有效。Unfortunately, there isn't a way to fully prevent this, and a generic
ICloneable<T>
with no type constraints is enough. Your constraint only limits possible parameters to classes which themselves implement it, which doesn't mean they are the ones currently being implemented.In other words, if a
Cow
implementsICloneable<Cow>
, you will still easily makeSheep
implementICloneable<Cow>
.I would simply use
ICloneable<T>
without constraints for two reasons:I seriously doubt you will ever make a mistake of using a wrong type parameter.
Interfaces are meant to be contracts for other parts of code, not to be used to code on autopilot. If a part of a code expects
ICloneable<Cow>
and you pass aSheep
which can do that, it seems perfectly valid from that point.