记录结构

注意

本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。

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

可以在 规范一文中详细了解将功能规范采用 C# 语言标准的过程。

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

记录结构的语法如下所示:

record_struct_declaration
    : attributes? struct_modifier* 'partial'? 'record' 'struct' identifier type_parameter_list?
      parameter_list? struct_interfaces? type_parameter_constraints_clause* record_struct_body
    ;

record_struct_body
    : struct_body
    | ';'
    ;

记录结构类型是值类型,与其他结构类型一样。 它们隐式继承自类 System.ValueType。 记录结构的修饰符和成员受到与结构体相同的限制(类型的可访问性、成员上的修饰符、base(...) 实例构造函数初始值设定项、构造函数中 this 的明确赋值、析构函数等)。 记录结构在无参数实例构造函数和字段初始值设定项方面也将遵循与结构体相同的规则,但本文档假设我们将普遍解除这些限制。

请参阅 §16.4.9 请参阅 无参数结构构造函数 规范。

记录结构不能使用 ref 修饰符。

一个部分记录结构体的最多一个部分类型声明可以提供 parameter_listparameter_list 可能为空。

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

记录结构体的成员

除了记录结构正文中声明的成员外,记录结构类型还具有其他合成成员。 除非在记录结构体正文中声明了一个具有“匹配”签名的成员,或者继承了一个可访问的具有“匹配“签名的具体非虚拟成员,否则成员将被合成。 如果两个成员具有相同的签名,或被视为在继承方案中“隐藏”,则两个成员被视为匹配。 请参阅“签名和重载”§7.6。 将记录结构体的成员命名为“Clone”是错误的。

记录结构体的实例字段具有不安全类型是错误的。

记录结构体不允许声明析构函数。

合成成员如下所示:

相等性成员

合成的相等性成员与记录类中的相等成员类似(该类型的 Equalsobject 类型的 Equals、该类型的 ==!= 运算符),
除了缺少 EqualityContract、null 校验或继承。

记录结构体实现 System.IEquatable<R>,并包含一个经过合成的强类型重载 Equals(R other),其中 R 是记录结构体。 方法为 public。 该方法可以显式声明。 如果显式声明与预期的签名或辅助功能不匹配,则会出现错误。

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

public readonly bool Equals(R other);

当且仅当记录结构体中每个实例字段 fieldNSystem.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) 值(其中 TN 是字段类型)为 true 时,合成 Equals(R) 才会返回 true

记录结构体包含合成的 ==!= 运算符,相当于如下声明的运算符:

public static bool operator==(R r1, R r2)
    => r1.Equals(r2);
public static bool operator!=(R r1, R r2)
    => !(r1 == r2);

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

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

public override readonly bool Equals(object? obj);

如果显式声明此替代,则会出现错误。 合成的重写返回 other is R temp && Equals(temp),其中 R 是记录结构体。

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

public override readonly int GetHashCode();

该方法可以显式声明。

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

GetHashCode() 的合成重写会返回 int 的结果,该结果是每个实例字段 fieldNSystem.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) 值与 TNfieldN 类型相结合的结果。

例如,请考虑以下记录结构:

record struct R1(T1 P1, T2 P2);

对于这个记录结构体来说,合成的平等成员应该如下所示:

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }
    public override bool Equals(object? obj) => obj is R1 temp && Equals(temp);
    public bool Equals(R1 other)
    {
        return
            EqualityComparer<T1>.Default.Equals(P1, other.P1) &&
            EqualityComparer<T2>.Default.Equals(P2, other.P2);
    }
    public static bool operator==(R1 r1, R1 r2)
        => r1.Equals(r2);
    public static bool operator!=(R1 r1, R1 r2)
        => !(r1 == r2);    
    public override int GetHashCode()
    {
        return Combine(
            EqualityComparer<T1>.Default.GetHashCode(P1),
            EqualityComparer<T2>.Default.GetHashCode(P2));
    }
}

打印成员:PrintMembers 和 ToString 方法

记录结构包括等效于声明的方法的合成方法,如下所示:

private bool PrintMembers(System.Text.StringBuilder builder);

该方法执行以下操作:

  1. 对于记录结构的每个可打印成员(非静态公共字段和可读属性成员),将该成员的名称后接“ = ”,再接成员的值,并以“,”分隔。
  2. 如果记录结构具有可打印成员,则返回 true。

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

如果记录的可打印成员不包含具有非 readonlyget 访问器的可读属性,那么合成的 PrintMembers 就是 readonly。 对于 PrintMembers 方法 readonly 来说,记录字段必须是 readonly 没有任何要求。

可以显式声明 PrintMembers 方法。 如果显式声明与预期的签名或辅助功能不匹配,则会出现错误。

记录结构包括等效于声明的方法的合成方法,如下所示:

public override string ToString();

如果记录结构体的 PrintMembers 方法是 readonly,那么合成的 ToString() 方法就是 readonly

该方法可以显式声明。 如果显式声明与预期的签名或辅助功能不匹配,则会出现错误。

合成方法:

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

例如,请考虑以下记录结构:

record struct R1(T1 P1, T2 P2);

对于这个记录结构体来说,合成的打印成员应该如下所示:

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }

    private bool PrintMembers(StringBuilder builder)
    {
        builder.Append(nameof(P1));
        builder.Append(" = ");
        builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if P1 has a value type
        builder.Append(", ");

        builder.Append(nameof(P2));
        builder.Append(" = ");
        builder.Append(this.P2); // or builder.Append(this.P2.ToString()); if P2 has 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();
    }
}

位置记录结构体成员

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

主构造函数

记录结构具有一个公共构造函数,其签名对应于类型声明的值参数。 这称为类型的主要构造函数。 在结构体中同时存在主构造函数和具有相同签名的构造函数是错误的。 如果类型声明不包含参数列表,则不会生成主构造函数。

record struct R1
{
    public R1() { } // ok
}

record struct R2()
{
    public R2() { } // error: 'R2' already defines constructor with same parameter types
}

记录结构中的实例字段声明允许包含变量初始化器。 如果没有主构造函数,实例初始值设定项将作为无参数构造函数的一部分执行。 否则,在运行时,主构造函数将执行记录结构正文中显示的实例初始值设定项。

如果记录结构具有主构造函数,则任何用户定义的构造函数都必须有一个显式 this 构造函数初始值设定项,该初始值设定项调用主构造函数或显式声明的构造函数。

主构造函数的参数和记录结构体的成员都在实例字段或属性初始化器的范围内。 实例成员在这些地方会产生错误(类似于实例成员在今天的常规构造函数初始值设定项中的范围,但使用时是一个错误),而主构造函数的参数在范围内可使用,并且会覆盖成员。 静态成员也可以使用。

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

结构实例构造函数的明确分配规则适用于记录结构的主要构造函数。 例如,下面是一个错误:

record struct Pos(int X) // definite assignment error in primary constructor
{
    private int x;
    public int X { get { return x; } set { x = value; } } = X;
}

性能

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

对于记录结构体:

  • 如果记录结构体具有 readonly 修饰符,则会创建公共 getinit 自动属性,否则会创建 getset 自动属性。 这两种类型的集访问器(setinit)都被视为“匹配”。 因此,用户可以声明仅初始化属性来代替合成可变属性。 具有匹配类型的继承 abstract 属性被重写。 如果记录结构具有具有预期名称和类型的实例字段,则不会创建自动属性。 如果继承的属性没有 publicgetset/init 访问器,则这是一个错误。 如果继承的属性或字段处于隐藏状态,则为错误。
    自动属性初始化为相应的主构造函数参数的值。 通过对语法上应用于相应记录结构体参数的属性使用 property:field: 目标,可以将属性应用于合成的自动属性及其支持字段。

析构

具有至少一个参数的位置记录结构体合成一个名为 Deconstruct 的公共 void 返回实例方法,并为主构造函数声明的每个参数声明 out 参数。 解构方法的每个参数都具有与主构造函数声明的对应参数相同的类型。 该方法的正文会将解构方法的每个参数赋值给访问同名成员的实例成员。 如果在正文中访问的实例成员不包括具有非 readonlyget 访问器的属性,那么合成的 Deconstruct 方法就是 readonly。 该方法可以显式声明。 如果显式声明与预期的签名或辅助功能不匹配或静态,则会出现错误。

允许在结构体上使用 with 表达式

现在,with 表达式中的接收器可以是结构类型。

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

对于结构体类型的接收器,首先复制接收器,然后以对转换结果的字段或属性访问赋值的相同方式处理每个 member_initializer。 赋值按词法顺序进行处理。

记录的改进

允许 record class

记录类型的现有语法允许 record class 的含义与 record相同:

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

允许用户定义的位置参数成员成为字段

请参见https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-10-05.md#changing-the-member-type-of-a-primary-constructor-parameter

如果记录具有或继承具有预期名称和类型的实例字段,则不会创建自动属性。

允许结构中的无参数构造函数和成员初始化器

请参阅 无参数结构构造函数 规范。

开放性问题

  • 如何识别元数据中的记录结构? (我们没有难以形容的克隆方法可以利用...)

已回答

  • 确认我们要保留 PrintMembers 设计(返回 bool 的单独方法)(答案:是)
  • 确认我们不允许 record ref struct(与 IEquatable<RefStruct> 和 ref 字段有关的问题)(答案:是)
  • 确认平等成员的实现。 另一种方法是,合成的 bool Equals(R other)bool Equals(object? other) 和运算符都直接委托给 ValueType.Equals。 (答:是)
  • 确认在存在主构造函数时,我们希望允许使用字段初始化器。 我们是否还希望允许使用无参数结构构造函数(激活器问题显然已经解决)? (答案:是,应在 LDM 中审查更新的规格)
  • 关于 Combine 方法,我们想要说多少呢? (答:尽可能少)
  • 我们是否应禁止具有复制构造函数签名的用户定义构造函数? (答:否,记录结构规范中没有复制构造函数的概念)
  • 确认我们要禁止名为“Clone”的成员。 (答:正确)
  • 仔细检查合成 Equals 逻辑在功能上是否等效于运行时实现(例如 float.NaN)(答:在 LDM 中确认)
  • 是否可以将字段或属性目标属性放在位置参数列表中? (答案:是,与记录类相同)
  • 关于泛型的 with? (答:不在 C# 10 的范围内)
  • 是否应 GetHashCode 包含类型本身的哈希,才能在 record struct S1;record struct S2;之间获取不同的值? (答:否)