自反类型参数约束:X其中T:X T 。 ‒有更简单的替代方案吗?

发布于 2024-12-27 04:51:58 字数 1373 浏览 2 评论 0 原文

我经常通过向简单的接口添加自引用(“自反”)类型参数约束来使简单的接口变得更加复杂。例如,我可以将其转换

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 似乎是递归的,人们可能想知道为什么编译器不会卡住在无限循环中,推理,“如果 TX,则 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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

夜深人未静 2025-01-03 04:51:58

主要优点:实现类型现在可以引用自身而不是其基类型,从而减少类型转换的需要

虽然看起来似乎是引用自身的类型约束,但它强制实现类型执行以下操作一样的,实际上不是这样的。人们使用此模式来尝试表达“此方法的重写必须返回重写类的类型”形式的模式,但这实际上并不是类型系统表达或强制执行的约束。我在这里举一个例子:

https://ericlippert.com/2011/02 /02/好奇和好奇/

虽然这非常好,但我也注意到这些类型参数约束并不直观,并且在更复杂的场景中往往变得非常难以理解

。我尽量避免这种模式。很难推理。

有谁知道另一种 C# 代码模式可以实现相同的效果或类似的效果,但以更容易掌握的方式?

不是在 C# 中,不是。如果您对这类事情感兴趣,您可以考虑查看 Haskell 类型系统; Haskell 的“高级类型”可以表示这些类型的类型模式。

声明X;其中 T : X 似乎是递归的,人们可能想知道为什么编译器不会陷入无限循环,推理​​“如果 T 是一个 X,那么 X 实际上是一个X…>>。”

在推理此类简单关系时,编译器永远不会陷入无限循环。然而,具有逆变性的泛型类型的名义子类型通常是不可判定的。有一些方法可以强制编译器陷入无限倒退,而 C# 编译器在踏上无限倒退之前不会检测到这些情况并阻止它们。 (然而,我希望在 Roslyn 编译器中添加对此的检测,但我们会看到的。)

如果您对此感兴趣,请参阅我关于该主题的文章。您还需要阅读链接到的论文。

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

Main advantage: An implementing type can now refer to itself instead of its base type, reducing the need for type-casting

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/

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

Yep. I try to avoid this pattern. It's hard to reason about.

Does anyone know of another C# code pattern that achieves the same effect or something similar, but in an easier-to-grasp fashion?

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 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>…>>."

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/

才能让你更想念 2025-01-03 04:51:58

不幸的是,没有办法完全防止这种情况,并且没有类型约束的通用 ICloneable 就足够了。您的约束仅将可能的参数限制为本身实现它的类,这并不意味着它们是当前正在实现的类。

换句话说,如果 Cow 实现了 ICloneable,您仍然可以轻松地让 Sheep 实现 ICloneable代码>.

我会简单地使用 ICloneable 而没有任何限制,原因有两个:

  1. 我严重怀疑您是否会犯下使用错误类型参数的错误。

  2. 接口旨在成为代码其他部分的契约,而不是用于在自动驾驶仪上进行编码。如果代码的一部分需要 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 implements ICloneable<Cow>, you will still easily make Sheep implement ICloneable<Cow>.

I would simply use ICloneable<T> without constraints for two reasons:

  1. I seriously doubt you will ever make a mistake of using a wrong type parameter.

  2. 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 a Sheep which can do that, it seems perfectly valid from that point.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文