为什么在具有主构造函数的记录中需要显式的“this”构造函数初始值设定项?

Why is an explicit `this` constructor initializer required in records with a primary constructor?

在 C# 9 中,我们可以创建位置记录,使它们获得构造函数,规范草案将其称为 primary constructor。我们也可以创建自定义构造函数,但如规范中所述:

If a record has a primary constructor, any user-defined constructor, except "copy constructor" must have an explicit this constructor initializer.

所以这是不允许的:

public record A(string Foo, int Bar)
{
    public A(MyClass x)
    {
        Foo = x.Name;
        Bar = x.Number;
    }
}

并且确实导致 CS8862“在带有参数列表的记录中声明的构造函数必须具有 'this' 构造函数初始值设定项。”我们必须写:

public record A(string Foo, int Bar)
{
    public A(MyClass x) : this(x.Name, x.Number) {}
}

相反。在这种情况下,这几乎不是问题,但可以想象一个更长的初始化逻辑,它不适合 this 构造函数初始化程序。

问题是:为什么会有这个限制?我猜解除它会以某种方式打破记录的某些功能,但它是一个足够新的功能,我无法想出一种方法来做到这一点。自动生成的主构造函数是否做了一些对记录正常工作至关重要的事情,因此必须调用它?


我不确定我是否有明确的答案,但以下是我根据所读内容得出的想法。这个

public record MyRecord(string foo, int bar);

相当于:

public class MyRecord 
{
    string foo { get; init; } // Code correction - set to init
    int bar { get; init; }
}

构造函数和属性都在一行中推断出来。虽然我确信可以向这个推断的构造集添加具体赋值(并且可能会在未来的版本中),但在第一遍中使用构造函数链接可能更容易。作为在某些 类(通常不是 pocos)中使用过链式构造函数的人,这是有道理的。但是,我不确定我会重载构造函数多少次,而且我没有看到一个巨大的好处,至少在架构上,按照你的方式去做。

基于快速的 Linqpad 反编译,是的,看起来这个构造函数实际上正在做一些工作,否则由于不知道如何将类型映射到记录属性而可能无法推断。

public A(string Foo, int Bar)
{
    this.Foo = Foo;
    this.Bar = Bar;
    base..ctor();
}

public A(MyClass x)
    : this(x.Name, x.Number)
{
}

记录需要通过构造函数初始化所有属性。如果可以在不显式调用 this 的情况下在附加构造函数中任意映射属性,则没有(立即显而易见的)方法来确保满足每个 属性 参数要求。因此,需要调用 this(params) 来执行 属性 映射。

这是因为主构造函数参数有点特殊 - 它们在整个记录初始化范围内。猜猜下面的程序打印什么:

System.Console.WriteLine(new Foo(Bar: 42).Baz);

public record Foo(int Bar) {
    public int Bar => 41;
    public int Baz = Bar;
}

4142?

答案是...

请击鼓...

42!

这是怎么回事?

在记录初始化期间,任何对 Bar 的引用都不是对 属性 Bar 的引用,而是对主构造函数参数 Bar 的引用。

这意味着必须调用主构造函数。否则在这种情况下会发生什么:

System.Console.WriteLine(new Foo().Baz);

public record Foo(int Bar) {
    public Foo(){}
    public int Bar => 41;
    public int Baz = Bar; //  What is Bar here when the primary constructor isn't called.
}

放在一边

Bar 参数仅在初始化期间在范围内。初始化后 Bar 属性 就在范围内。如果我们稍微改变我们的例子:

System.Console.WriteLine(new Foo(Bar: 42).Baz);

public record Foo(int Bar) {
    public int Bar => 41;
    public int Baz => Bar; //Note this is `=>` not `=`
}

它会打印 41.