记录

注意

本文是特性规范。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 ECMA 规范。

功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。

可以在有关规范的文章中了解更多有关将功能规范子块纳入 C# 语言标准的过程。

支持者问题:https://github.com/dotnet/csharplang/issues/39

此提案旨在跟踪与 C# 9 记录特性相关的规范,该规范已获得 C# 语言设计团队的批准。

记录的语法如下:

record_declaration
    : attributes? class_modifier* 'partial'? 'record' identifier type_parameter_list?
      parameter_list? record_base? type_parameter_constraints_clause* record_body
    ;

record_base
    : ':' class_type argument_list?
    | ':' interface_type_list
    | ':' class_type argument_list? ',' interface_type_list
    ;

record_body
    : '{' class_member_declaration* '}' ';'?
    | ';'
    ;

记录类型是引用类型,类似于类声明。 如果record_base不包含argument_list,则记录中提供record_declarationparameter_list是错误的。 一个部分记录的最多一个部分类型声明可以提供 parameter_list

记录参数不能使用 refoutthis 修饰符(但允许使用 inparams)。

继承

除非类是 object,否则记录不能从类继承,类也不能从记录继承。 记录可以从其他记录继承。

记录类型的成员

除了记录正文中声明的成员外,记录类型还具有其他合成成员。 除非在记录正文中声明了一个具有“匹配”签名的成员,或者继承了一个可访问的具有“匹配“签名的具体非虚拟成员,否则成员将被合成。 匹配的成员会阻止编译器生成该成员,而不是任何其他合成的成员。 如果两个成员具有相同的签名,或被视为在继承方案中“隐藏”,则两个成员被视为匹配。 将记录成员命名为“Clone”是错误的。 记录的实例字段具有顶级指针类型是错误的。 允许嵌套指针类型(如指针数组)。

合成成员如下:

相等性成员

如果记录派生自 object,则记录类型包括一个合成的只读属性,该属性等效于如以下方式声明的属性:

Type EqualityContract { get; }

记录类型为 private时,此属性为 sealed。 否则,该属性为 virtualprotected。 可以显式声明该属性。 如果显式声明与预期的签名或可访问性不匹配,或者显式声明不允许在派生类型中重写它,并且记录类型不为 sealed,则会出现错误。

如果记录类型派生自基本记录类型 Base,则记录类型包括一个合成的只读属性,相当于如下声明的属性:

protected override Type EqualityContract { get; }

可以显式声明该属性。 如果显式声明与预期的签名或可访问性不匹配,或者显式声明不允许在派生类型中重写它,并且记录类型不为 sealed,则会出现错误。 如果合成的属性或显式声明的属性未能覆盖在记录类型 Base 中具有此签名的属性(例如,如果该属性在 Base 中缺失,或者被密封,或者不是虚拟的,等等),则会出现错误。 合成的属性返回 typeof(R),其中 R 是记录类型。

记录类型实现 System.IEquatable<R>,并包含一个经过合成的强类型重载 Equals(R? other),其中 R 是记录类型。 方法为 public;除非记录类型为 virtual,否则方法为 sealed。 该方法可以显式声明。 如果显式声明与预期的签名或可访问性不匹配,或者显式声明不允许在派生类型中重写它,并且记录类型不为 sealed,则会出现错误。

如果 Equals(R? other) 是用户定义的(不是合成的),但 GetHashCode 不是,则会生成警告。

public virtual bool Equals(R? other);

当且仅当以下每一项都 Equals(R?) 时,合成的 true 返回 true

  • other 不是 null,并且
  • 对于非继承记录类型中的每个实例字段 fieldNSystem.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) 的值(TN 为字段类型),和
  • 如果存在基本记录类型,则 base.Equals(other) 的值(对 public virtual bool Equals(Base? other) 的非虚拟调用);否则为 EqualityContract == other.EqualityContract 的值。

记录类型包括合成的 == 运算符和 != 运算符,这些运算符等同于如下声明的运算符:

public static bool operator==(R? left, R? right)
    => (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R? left, R? right)
    => !(left == right);

== 运算符调用的 Equals 方法是上面指定的 Equals(R? other) 方法。 != 运算符委托给 == 运算符。 如果显式声明这些运算符,则会出现错误。

如果记录类型派生自基本记录类型 Base,则记录类型包括一个合成的重写,相当于如下声明的方法:

public sealed override bool Equals(Base? other);

如果显式声明此替代,则会出现错误。 如果该方法没有重写记录类型 Base 中具有相同签名的方法(例如,如果该方法在 Base 中缺失、密封或非虚拟等),则会出错。 合成的重写会返回 Equals((object?)other)

记录类型包括一个合成的重写,相当于如下声明的方法:

public override bool Equals(object? obj);

如果显式声明此替代,则会出现错误。 如果方法没有覆写 object.Equals(object? obj)(例如,由于中间基类型中的隐藏等),则这是一个错误。 合成的重写返回 Equals(other as R),其中 R 是记录类型。

记录类型包括一个合成的重写,相当于如下声明的方法:

public override int GetHashCode();

该方法可以显式声明。 如果显式声明不允许在派生类型中重写它,并且记录类型不是 sealed,则会生成错误。 如果合成或显式声明的方法不重写 object.GetHashCode()(例如,由于中间基类型等中的隐藏),则会生成错误。

如果显式声明 Equals(R?)GetHashCode() 之一,但另一种方法不显式,则会报告警告。

GetHashCode() 的合成重写返回组合以下值的 int 结果:

  • 对于非继承记录类型中的每个实例字段 fieldNSystem.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) 的值(TN 为字段类型),和
  • 如果有基记录类型,则为 base.GetHashCode()的值;否则为 System.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract)的值。

例如,请考虑以下记录类型:

record R1(T1 P1);
record R2(T1 P1, T2 P2) : R1(P1);
record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2);

对于这些记录类型,合成的相等性成员如下所示:

class R1 : IEquatable<R1>
{
    public T1 P1 { get; init; }
    protected virtual Type EqualityContract => typeof(R1);
    public override bool Equals(object? obj) => Equals(obj as R1);
    public virtual bool Equals(R1? other)
    {
        return !(other is null) &&
            EqualityContract == other.EqualityContract &&
            EqualityComparer<T1>.Default.Equals(P1, other.P1);
    }
    public static bool operator==(R1? left, R1? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R1? left, R1? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(EqualityComparer<Type>.Default.GetHashCode(EqualityContract),
            EqualityComparer<T1>.Default.GetHashCode(P1));
    }
}

class R2 : R1, IEquatable<R2>
{
    public T2 P2 { get; init; }
    protected override Type EqualityContract => typeof(R2);
    public override bool Equals(object? obj) => Equals(obj as R2);
    public sealed override bool Equals(R1? other) => Equals((object?)other);
    public virtual bool Equals(R2? other)
    {
        return base.Equals((R1?)other) &&
            EqualityComparer<T2>.Default.Equals(P2, other.P2);
    }
    public static bool operator==(R2? left, R2? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R2? left, R2? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode(),
            EqualityComparer<T2>.Default.GetHashCode(P2));
    }
}

class R3 : R2, IEquatable<R3>
{
    public T3 P3 { get; init; }
    protected override Type EqualityContract => typeof(R3);
    public override bool Equals(object? obj) => Equals(obj as R3);
    public sealed override bool Equals(R2? other) => Equals((object?)other);
    public virtual bool Equals(R3? other)
    {
        return base.Equals((R2?)other) &&
            EqualityComparer<T3>.Default.Equals(P3, other.P3);
    }
    public static bool operator==(R3? left, R3? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R3? left, R3? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode(),
            EqualityComparer<T3>.Default.GetHashCode(P3));
    }
}

复制和克隆成员

记录类型包含两个复制成员:

  • 采用记录类型的单个参数的构造函数。 它被称为“复制构造函数”。
  • 具有编译器保留名称的合成公共无参数实例“clone”方法

复制构造函数的目的是将状态从参数复制到要创建的新实例。 此构造函数不运行记录声明中存在的任何实例字段/属性初始值设定项。 如果未显式声明构造函数,编译器将合成构造函数。 如果记录已密封,则构造函数将是专用,否则将受到保护。 除非密封记录,否则显式声明的复制构造函数必须是公共的或受保护的。 构造函数必须执行的第一件事是调用基的复制构造函数;或者如果记录继承自对象,则调用无参数对象构造函数。 如果用户定义的复制构造函数使用不符合此要求的隐式或显式构造函数初始值设定项,则会报告错误。 调用基复制构造函数后,系统生成的复制构造函数会复制记录类型中所有实例字段的值,这些字段可以是隐式或显式声明的。 复制构造函数的唯一存在(无论是显式的还是隐式的)不会阻止自动添加默认实例构造函数。

如果基本记录中存在虚拟“clone”方法,则合成的“clone”方法将重写它,并且该方法的返回类型是当前包含类型。 如果基础记录的克隆方法被封闭,则会产生错误。 如果基本记录中不存在虚拟“clone”方法,则 clone 方法的返回类型为包含类型,并且该方法为虚拟,除非记录是密封的或抽象的。 如果包含记录是抽象的,则合成的克隆方法也是抽象的。 如果“clone”方法不是抽象方法,它将返回对复制构造函数的调用的结果。

打印成员:PrintMembers 和 ToString 方法

如果记录派生自 object,则该记录包含一个与如下声明的方法等效的合成方法:

bool PrintMembers(System.Text.StringBuilder builder);

如果记录类型是private,则方法是sealed。 否则,方法是 virtualprotected

方法:

  1. 如果方法存在并且记录有可打印的成员,则调用方法 System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack()
  2. 对于每个记录的可输出成员(非静态公共字段和可读属性成员),附加该成员的名称,后跟“=”,再后跟成员的值,并用“,”分隔。
  3. 如果记录具有可输出成员,则返回 true。

对于具有值类型的成员,我们将使用可用于目标平台的最有效方法将其值转换为字符串表示形式。 目前,这意味着在传递给 ToString 之前调用 StringBuilder.Append

如果记录类型派生自基本记录 Base,则记录包括一个合成的重写,相当于如下声明的方法:

protected override bool PrintMembers(StringBuilder builder);

如果记录没有可打印成员,该方法将使用一个参数(其 PrintMembers 参数)调用基类 builder 方法,并返回结果。

否则,方法:

  1. 使用一个参数(其 builder 参数)调用基 PrintMembers 方法,
  2. 如果 PrintMembers 方法返回 true,则向生成器追加“,”,
  3. 对于每个记录的可输出成员,附加该成员的名称,后跟“=”,再后跟成员的值:this.member(或对于值类型则使用 this.member.ToString()),并用“,”分隔,
  4. 返回 true。

PrintMembers 方法可以显式声明。 如果显式声明与预期的签名或可访问性不匹配,或者显式声明不允许在派生类型中重写它,并且记录类型不为 sealed,则会出现错误。

类型包括一个合成的方式,相当于如下声明的方法:

public override string ToString();

该方法可以显式声明。 如果显式声明与预期的签名或可访问性不匹配,或者显式声明不允许在派生类型中重写它,并且记录类型不为 sealed,则会出现错误。 如果合成或显式声明的方法不重写 object.ToString()(例如,由于中间基类型等中的隐藏),则会生成错误。

合成方法:

  1. 创建 StringBuilder 实例,
  2. 将记录名称追加到生成器,后跟 " { ",
  3. 调用记录的 PrintMembers 方法,为其提供生成器;如果返回 true,则后跟 " "。
  4. 追加 "}",
  5. 返回具有 builder.ToString() 的生成器内容。

例如,请考虑以下记录类型:

record R1(T1 P1);
record R2(T1 P1, T2 P2, T3 P3) : R1(P1);

对于这些记录类型,合成的输出成员如下所示:

class R1 : IEquatable<R1>
{
    public T1 P1 { get; init; }
    
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append(nameof(P1));
        builder.Append(" = ");
        builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if T1 is a value type
        
        return true;
    }
    
    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R1));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

class R2 : R1, IEquatable<R2>
{
    public T2 P2 { get; init; }
    public T3 P3 { get; init; }
    
    protected override bool PrintMembers(StringBuilder builder)
    {
        if (base.PrintMembers(builder))
            builder.Append(", ");
            
        builder.Append(nameof(P2));
        builder.Append(" = ");
        builder.Append(this.P2); // or builder.Append(this.P2); if T2 is a value type
        
        builder.Append(", ");
        
        builder.Append(nameof(P3));
        builder.Append(" = ");
        builder.Append(this.P3); // or builder.Append(this.P3); if T3 is a value type
        
        return true;
    }
    
    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R2));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

位置记录成员

除了上述成员之外,具有参数列表(“位置记录”)的记录还会合成具有与上述成员相同条件的其他成员。

主构造函数

记录类型具有一个公共构造函数,其签名对应于类型声明的值参数。 这称为类型的主要构造函数,并导致隐式声明的默认类构造函数(如果存在)被禁止。 在类中已经存在具有相同签名的主构造函数和构造函数是错误的。

在运行时,主构造函数

  1. 执行类体中出现的实例初始化器

  2. 使用 record_base 子句中提供的参数(如果存在)调用基类构造函数

如果记录具有主构造函数,则任何用户定义的构造函数(“复制构造函数”除外)都必须具有显式 this 构造函数初始值设定项。

主构造函数的参数以及记录的成员都在 argument_list子句的 record_base 范围内,以及实例字段或属性的初始值设定项内。 实例成员在这些地方会产生错误(类似于实例成员在今天的常规构造函数初始值设定项中的范围,但使用时是一个错误),而主构造函数的参数在范围内可使用,并且会覆盖成员。 静态成员也是可用的,类似于今天普通构造函数中基类调用和初始值设定项的当前工作方式。

如果未读取主构造函数的参数,则会生成警告。

argument_list 中声明的表达式变量在 argument_list 范围内。 与常规构造函数初始值设定项的参数列表中相同的隐藏规则也适用。

属性

对于记录类型声明的每个记录参数,都有一个相应的公共属性成员,其名称和类型取自值参数声明。

作为记录:

  • 创建公共 getinit 自动属性(请参阅单独的 init 访问器规范)。 具有匹配类型的继承 abstract 属性被重写。 如果继承的属性没有 public 可重写的 getinit 访问器,则会生成错误。 如果继承的属性被隐藏,则是一个错误。
    自动属性被初始化为相应的主构造函数参数的值。 通过对语法上应用于相应记录参数的属性使用 property:field: 目标,可以将属性应用于合成的自动属性及其支持字段。

析构

具有至少一个参数的位置记录合成一个名为解构的公共 void 返回实例方法,并为主构造函数声明的每个参数声明 out 参数。 Deconstruct 方法的每个参数都与主构造函数声明的相应参数具有相同的类型。 方法的主体将同名实例属性的值赋给 Deconstruct 方法的每个参数。 该方法可以显式声明。 如果显式声明与预期的签名或访问权限不匹配,或者是静态的,则会出现错误。

以下示例显示了一个位置记录 R 及其由编译器生成的 Deconstruct 方法,以及其用法:

public record R(int P1, string P2 = "xyz")
{
    public void Deconstruct(out int P1, out string P2)
    {
        P1 = this.P1;
        P2 = this.P2;
    }
}

class Program
{
    static void Main()
    {
        R r = new R(12);
        (int p1, string p2) = r;
        Console.WriteLine($"p1: {p1}, p2: {p2}");
    }
}

with 表达式

with 表达式是使用以下语法的新表达式。

with_expression
    : switch_expression
    | switch_expression 'with' '{' member_initializer_list? '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : identifier '=' expression
    ;

不允许将 with 表达式作为语句。

with 表达式允许“非破坏性突变”,旨在生成一个接收者表达式的副本,并在 member_initializer_list中对赋值进行修改。

有效的 with 表达式具有具有非 void 类型的接收方。 接收方类型必须是记录。

with 表达式的右侧是一个 member_initializer_list,它具有一系列对标识符的赋值,该标识符必须是接收方类型的可访问实例字段或属性。

首先,调用接收者的“clone”方法(如上所述),然后将其结果转换为接收者的类型。 然后,每个 member_initializer 的处理方式与转换结果的字段赋值或属性访问相同。 赋值按词法顺序进行处理。