递归约束:DBase<T> : where T : DBase<T> 是什么意思?

3

我原本认为我理解了泛型约束,直到我遇到这个问题。

public class DBase<T> : DbContext, IDisposable where T : DBase<T>

如何把T作为DBase<T>

如果可以的话,这意味着什么?

这段代码能够通过编译并正常运行。我不是在修复问题。我只是不明白它。

它被用在这里:

    public class ChildDb : DBase<ChildDb>

对我来说,这仍然不可理解。它将自身作为类型参数传递?


3
这样,您可以将所有通用方法设置为要求T与声明类型相同的类型。请参阅https://blogs.msdn.microsoft.com/simonince/2008/06/12/generics-the-self-referencing-generics-pattern/。 - Charleh
4
这被称为“奇妙重复模板模式”(Curiously Repeating Template Pattern,CRTP)。 - John Wu
很有趣。你想把它们中的任何一个发布为答案,这样我就可以确认它了吗? - BWhite
通常用于创建具有可链接方法的流畅构建器。在这个示例中,您可以看到,这种模式对于让基类拥有返回派生类实例的方法非常有用。 - Reza Aghaei
这三个答案中的任何一个都似乎是有效的。我会等一周,看哪个被投票赞同。 - BWhite
3个回答

3

如何将 T 设为 DBase<T>

没有限制阻止泛型参数从自身派生。虽然你所给出的示例不太容易理解,但是对于一个 顶点 / 点,这种情况就很常见了。

摘自维基百科:

enter image description here

在几何学中,顶点(复数:vertices或vertexes)是两条或多条曲线、直线或边相交的点。根据这个定义,两条线相交成角的点以及多边形和多面体的角落都是顶点。如何描述顶点(一个点)?
// very simplified example
public class Vertex
{
  public int X { get; set; }
  public int Y { get; set; }
}

现在,我们如何向这个类添加一组关联的顶点,但只允许从此类派生的内容?
public class Vertex<TVertex> : Vertex
  where TVertex : Vertex<TVertex>
{
  public IEnumerable<TVertex> Vertices { get; set; }
}

这是一个通用版本的说法:

public Vertex2
{
  public IENumerable<Vertex2> Vertices { get; set; }
}

然而,当我从Vertex2派生时,我的Vertices将始终是IEnumerable<Vertex2>,并且允许Vertices成为派生类的正确方法是使用这种类型的自引用泛型。

对不起Erik,我在细节中失去了重点。递归给我带来了什么好处?

使用Vertex2,我们派生类型失去了对其他派生属性的访问

public class MyVertex2: Vertex2
{
  public int Id { get; set; }
}

所以

var a = new MyVertex2 {Id = 1 };
var b = new MyVertex2 { Id = 2 };
a.Vertices = new List<Vertex2> { b };
b.Vertices = new List<Vertex2> { a };

// can't access Id because it's a Vertex2 not a MyVertex2
var bId = a.Vertices.First().Id;

当然你可以强制转换,但这样会在所有地方都进行转换(这不符合DRY原则)...而且如果它不是MyVertex类型的话(可能会出现MullReferencesException或InvalidCastException异常)。

public class MyVertex: Vertex<MyVertex>
{
  public int Id { get; set; }
}
var a = new MyVertex {Id = 1 };
var b = new MyVertex { Id = 2 };
a.Vertices = new List<MyVertex > { b };
b.Vertices = new List<MyVertex > { a };

var bId = a.Vertices.First().Id;
// or even
var aId = a.Vertices.First().Vertices.First();

每次我们导航到一个顶点时,我们得到的是正确的派生类型,而不是基类。

好的例子,但需要数学知识 :) - Rahul
对不起,Erik,我在细节中迷失了重点。通过递归,我获得了什么?这样做会更好吗:“<T> where T: Vertex”,其中Vertex是原始类而不是泛型。显然,我还是缺少了些什么。 - BWhite
@BWhite 如果它不是通用类,你无法导航到其他顶点,因为你没有 Vertices 类。 - Erik Philips
我明白了,谢谢澄清。总结一下,这主要是关于类能够使用正确的派生类型。我只是看着代码而不是它被调用的方式。 - BWhite

1
约翰·吴在评论中发布了一篇很棒的博客,其要点如下:
这个代码模式允许您声明一个必须被扩展的超类(可能不是您自己,如果您正在编写其他人将使用的库),但可以有一堆方法/签名(由您编写)返回T,但实际上将返回子类型的对象(不是由您编写/您无法知道),以便它们可以像大多数StringBuilder方法一样被链接使用(例如,返回StringBuilder本身,以便用户可以调用.Append().AppendLine()),而不需要从父类型(由您编写)向子类型(不是由您编写)进行转换(在未由您编写的代码中)。
有一个警告:它并不特别有用,因为只有继承树中最深的子类可以被实例化。避免使用它。

1
作为一个有用的例子,它允许您在基类中拥有一些返回派生类型的方法或属性。
例如,在具有可链接方法的流畅构建器中,假设我们有一个设置一些公共属性的基本构建器。这些方法应该输出什么类型?
请参见以下示例:
public abstract class Control
{
    public string Id { get; set; }
}
public abstract class ControlBuilder<TBuilder, TControl>
    where TBuilder : ControlBuilder<TBuilder, TControl>, new()
    where TControl : Control, new()
{
    protected TControl control;
    protected ControlBuilder()
    {
        control = new TControl();
    }
    public static TBuilder With()
    {
        return new TBuilder();
    }
    public TControl Build()
    {
        control;
    }
    public TBuilder Id(string id)
    {
        control.Id = id;
        return (TBuilder)this;
    }
}

没有将ControlBuilder<TBuilder, TControl>作为TBuilder的约束条件,你如何从Id方法中返回一个TBuilder?如果您返回它,那么在调用方法链中的.Id("something")之后,它将不显示派生类方法,只会显示ControlBuilder<TBuilder,TControl>的方法。如果您问为什么不返回ControlBuilder<TBuilder,TControl>,因为如果您返回它,在调用方法链中的.Id("something")之后,它将不显示派生类方法,只会显示ControlBuilder<TBuilder,TControl>的方法。假设我们创建了一个用于构建TextBoxTextBoxBuilder
public class TextBox : Control
{
    public string Text { get; set; }
}
public class TextBoxBuilder : ControlBuilder<TextBoxBuilder, TextBox>
{
    public TextBoxBuilder Text(string text)
    {
        control.Text = text;
        return this;
    }
}

现在我们可以按照预期使用它:

var txt = TextBoxBuilder.With().Id("textBox1").Text("Hello!").Build();

我知道这个例子看起来有点复杂,但是如果你试着去操作并回答“如果没有将ControlBuilder<TBuilder, TControl>作为TBuilder的约束条件,你如何从Id方法返回TBuilder”,你会发现如果没有这个约束条件,那么在调用TextBoxBuilder.With().Id("textBox1")方法后,TextBoxBuilder的方法将不会出现,你只能看到基本的ControlBuilder的方法。我建议你试着去操作一下,以便更好地理解。希望能帮到你 :) - Reza Aghaei

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