16 结构

16.1 常规

结构类似于类,它们表示可以包含数据成员和函数成员的数据结构。 但是,与类不同,结构是值类型,不需要堆分配。 变量 struct类型直接包含struct类型的数据,而类类型的变量包含对该数据的引用,后者称为对象。

注意:结构对于具有值语义的小型数据结构特别有用。 复数、坐标系中的点或字典中的键值对都是结构的典型示例。 这些数据结构的关键在于,它们很少有数据成员,它们不需要使用继承或引用语义,而是可以使用赋值复制值而不是引用的值语义方便地实现它们。 尾注

§8.3.5中所描述,C# 提供的简单类型,如intdoublebool,实际上都是结构体类型。

16.2 结构声明

16.2.1 概述

struct_declaration是声明新结构的type_declaration§14.8):

struct_declaration
    : non_record_struct_declaration
    | record_struct_declaration
    ;

non_record_struct_declaration
    : non_record_struct_without_positional_members
    | non_record_struct_with_positional_members
    ;

non_record_struct_without_positional_members
    : attributes? struct_modifier* 'ref'? 'partial'? 'struct'
      identifier type_parameter_list? struct_interfaces?
      type_parameter_constraints_clause* struct_body
    ;

non_record_struct_with_positional_members
    : attributes? struct_modifier* 'ref'? 'partial'? 'struct'
      identifier type_parameter_list? delimited_parameter_list struct_interfaces?
      type_parameter_constraints_clause* struct_body
    ;

有两种类型的结构: 非记录结构,由 non_record_struct_declaration声明, 记录结构由record_struct_declaration声明。 非记录结构是 C# 自语言成立以来支持的结构类型。 稍后会添加记录结构,并在 §16.5 中讨论。 这两种之间的差异在 §16.7 中讨论。

non_record_struct_declaration可以有两种几乎完全相同的形式之一:non_record_struct_without_positional_membersnon_record_struct_with_positional_members

non_record_struct_without_positional_members由一组可选的属性§23)组成,后跟一组可选的struct_modifier§16.2.2),后跟可选修饰符(§16.2.3),后跟可选的部分修饰符(§15.2.7),后refstruct一个关键字和一个用于命名结构的标识符, 后跟可选type_parameter_list规范(§15.2.3)),后跟可选的struct_interfaces规范(§16.2.5),后跟可选的type_parameter_constraints子句规范(§15.2.5),后跟struct_body§16.2.6),可选后跟分号。

non_record_struct_with_positional_members具有相同的语法,但需要delimited_parameter_list,如上述语法规则所示。 有关 delimited_parameter_list的讨论,请参阅 §15.11.6

除非struct_declaration也提供type_parameter_list,否则不得提供type_parameter_constraints_clause

提供type_parameter_liststruct_declaration是泛型结构声明。 此外,嵌套在泛型类声明或泛型结构声明中的任何结构本身都是泛型结构声明,因为应提供包含类型的类型参数来创建构造类型(§8.4)。

包含修饰ref不应具有struct_interfaces部分。

16.2.2 结构修饰符

struct_declaration 可以选择包括一系列 struct_modifier

struct_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'readonly'
    | 'file'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier§24.2)仅在不安全的代码(§24)中可用。

相同的修饰符在结构声明中多次出现,会导致编译时错误。

对于要与public修饰符组合的任何 protectedinternalprivatefile修饰符,这是编译时错误。

除了readonly,结构声明的修饰符与类声明(§15.2.2)的含义相同。

readonly 修饰符指示 struct_declaration 声明了一种实例不可变的类型。

只读结构具有以下约束:

  • 其每个实例字段也应声明 readonly
  • 它不应声明任何类似字段的事件(§15.8.2)。

将只读结构实例传递给方法时,其 this 处理方式类似于输入参数/参数,该参数禁止写入任何实例字段(构造函数除外)。

16.2.3 Ref 修饰符

修饰 ref 符指示 non_record_struct_declaration 声明在执行堆栈上分配实例的类型。 这些类型称为 ref 结构 类型。 ref修饰符声明实例可以包含类似 ref 的字段,不得从其安全上下文中复制(§16.8.15)。 用于确定 ref 结构的安全上下文的规则在 §16.8.15 中介绍。

如果在以下任一上下文中使用 ref 结构类型,则为编译时错误:

  • 作为数组的元素类型。
  • 作为类或结构中未使用 ref 修饰符的字段的声明类型。
  • 作为元组元素的类型。
  • 在异步方法中。
  • 在迭代器中。
  • 作为方法组从实例方法转换为委托类型的接收方类型。
  • 作为 lambda 表达式或本地函数中的捕获变量。

此外,以下限制适用于类型 ref struct

  • ref struct不应将类型装箱到System.ValueTypeSystem.Object装箱。
  • objectSystem.ValueType 中声明但未在 ref struct 类型中被替代的实例方法不应与通过该 ref struct 类型的接收器来调用。

注意:A ref struct 不应声明 async 实例方法,也不会在 yield return 实例方法中使用或 yield break 语句,因为隐式 this 参数不能在这些上下文中使用。 尾注

这些约束可确保类型的 ref struct 变量不引用不再有效的堆栈内存,也不引用不再有效的变量。

尽管类型 ref struct 可以实现接口,但该 ref struct 类型应实现该接口的所有实例成员,即使它们具有默认实现也是如此。

16.2.4 Partial 修饰符

修饰符 partial 指示此 struct_declaration 是分部类型声明。 包含命名空间或类型声明内具有相同名称的多个部分结构声明组合成一个结构声明,遵循 §15.2.7 中指定的规则。

16.2.5 结构接口

结构声明可能包括一个struct_interfaces规范,在这种情况下,该结构被认为是直接实现了所给的接口类型。 对于已构造的结构体类型,包括在泛型类型声明(§15.3.9.7)中声明的嵌套类型,通过用构造类型的对应type_argument替换给定接口中的每个type_parameter,可以获得每个实现的接口类型。

struct_interfaces
    : ':' interface_type_list
    ;

部分结构声明(§15.2.7)的多个部分接口的处理在 §15.2.4.3 中进一步讨论。

接口实现在 §19.6 中进一步讨论。

16.2.6 结构正文

结构struct_body定义结构的成员。

struct_body
    : '{' struct_member_declaration* '}' ';'?
    | ';'
    ;

struct_bodys{}{};;等效,struct_body且{…}{…};等效。

16.3 结构成员

16.3.1 常规

结构的成员由其 struct_member_declaration引入的成员、继承自类型“System.ValueType”的成员以及实现隐式提供的任何成员(§16.5.3)组成。

struct_member_declaration
    : constant_declaration
    | struct_field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | static_constructor_declaration
    | type_declaration
    | fixed_size_buffer_declaration   // unsafe code support
    ;

fixed_size_buffer_declaration§24.8.2)仅在不安全的代码(§24)中可用。

注意struct_member_declaration包括除finalizer_declaration以外的所有class_member_declaration替代项,并添加支持 ref 字段的struct_field_declaration§16.8.8.8.2)。 尾注

结构中的字段支持类中不支持的功能。 有关详细信息,请参阅 §16.8.8.2

除了 §16.8 中所述的差异外,§15.3 至 §15.12 中提供的类成员的说明也适用于结构成员。

16.3.2 只读成员

包含 readonly 修饰符的实例属性、索引器或事件的实例成员定义或访问器具有以下限制:

  • 参数 this 是引用 ref readonly
  • 该成员不应重新分配接收方的值 this 或实例字段。
  • 该成员不应重新分配接收方实例字段类事件(§15.8.2)的值。
  • 如果只读成员调用非只读成员,则必须复制所 this 引用的结构以对参数使用可写引用 this

注意: 实例字段包括用于自动实现属性的隐藏后盾字段(§15.7.4)。 尾注

示例:只读成员可以修改实例字段引用的对象的状态,即使只读成员无法重新分配该实例成员。 以下代码演示了重新分配和修改实例字段:

public struct S
{
    private List<string> messages;

    public S(IEnumerable<string> messages) =>
        this.messages = new List<string>(messages);

    public void InitializeMessages() =>
        messages = new List<string>();

    public readonly void AddMessage(string message)
    {
        if (messages == null)
        {
            throw new InvalidOperationException("Messages collection is not initialized.");
        }
        messages.Add(message);
    }
}

该方法readonlyAddMessage可以更改消息列表的状态。 该 InitializeMessages 成员可以清除并重新初始化消息列表。 在这种情况下 AddMessagereadonly 修饰符有效。 在这种情况下 InitializeMessages,添加 readonly 修饰符无效。 示例结束

16.4 主要构造函数

与非记录类一样,具有 delimited_parameter_list 的非记录结构具有实现提供的主要构造函数(§15.11.6)。 非记录类版本的语义也在此处应用,并由此子句中的文本进行扩充。

对于非记录类,实现应为每个参数提供专用的仅初始化字段。 但是,对于非记录结构,存储是读写的,并以某种未指定的方式提供。

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

16.5 记录结构

16.5.1 常规

记录结构是一种专用值类型,针对存储数据而不是行为进行优化。 它提供内置功能,通常需要非记录结构中的大量“样板”代码,例如基于值的相等性和容易的不可变性。

record_struct_declaration
    : attributes? struct_modifier* 'partial'? 'record' 'struct'
      identifier type_parameter_list? delimited_parameter_list? struct_interfaces?
      type_parameter_constraints_clause* struct_body
    ;

record_struct_declaration由一组可选的属性§23)组成,后跟一组可选的struct_modifier§16.2.2),后跟可选的部分修饰符(§15.2.7),后跟关键字recordstruct,后跟一个用于命名结构的关键字和标识符。 后跟可选type_parameter_list规范(§15.2.3),后跟可选delimited_parameter_list specification (§15.6.2.1),后跟可选的struct_interfaces规范(§16.2.5),后跟可选的type_parameter_constraints子句规范(§15.2.5),后跟struct_body(§16.2.6)。

具有delimited_parameter_list声明位置记录结构的record_struct_declaration

最多只能提供一个包含partialdelimited_parameter_list的record_struct_declaration

delimited_parameter_list中的参数不得具有refoutthis修饰符;但是,in允许修饰params符。

16.5.2 结构成员

对于要命名 Clone的记录结构的成员,这是一个错误。

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

16.5.3 隐式记录结构成员

16.5.3.1 常规

对于记录结构,成员由实现提供,除非在 struct_body 中声明具有“匹配”签名的成员或继承具有“匹配”签名的可访问的具体非虚拟成员。 匹配成员阻止实现仅提供该成员,而不提供任何其他提供的成员。 如果两个成员具有相同的签名,或被视为在继承方案中“隐藏”,则两个成员被视为匹配。 (请参阅签名和重载 §7.6

实现提供的成员在以下子项中进行了介绍。

16.5.3.2 主要构造函数

记录结构的主要构造函数与非记录结构(§16.4)的主构造函数类似,区别如下:每个参数值都存储在具有具有设置和 get 访问器的相应属性的对应专用实例字段中。

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

16.5.3.3 相等成员

提供的相等成员类似于记录类(§15.16.4.3),但缺少方法 EqualityContract、null 检查或继承。

记录结构 R 实现 System.IEquatable<R> 并包括合成的强类型重载 Equals(R other),这是公共的,如下所示:

public readonly bool Equals(R other);

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

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

如果记录结构中每个实例字段Equals(R)的值(其中字段true类型为fieldN)的值,则提供的System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN)字段应返回TNtrue

记录结构包括提供的 == 运算符和 != 与声明如下的运算符等效的运算符:

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将每个实例字段System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)的值fieldNTN类型fieldN相结合的结果。

示例:请考虑以下记录结构:

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 HashCode.Combine(
            EqualityComparer<T1>.Default.GetHashCode(P1),
            EqualityComparer<T2>.Default.GetHashCode(P2));
    }
}

示例结束

16.5.3.4 打印成员

记录结构包括提供的方法,声明如下:

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

此方法执行以下任务:

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

对于具有值类型的成员,其值应转换为字符串表示形式。

如果记录的可打印成员不包含具有非readonlyget 访问器的可读属性,则合成 PrintMembersreadonly。 不需要记录的字段readonlyPrintMembers是方法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();
    }
}

示例结束

16.5.3.5 位置记录结构成员

16.5.3.5.1 常规

除了提供上述子项中描述的成员外,位置记录结构(§16.2.1)也会导致实现为其他成员提供与其他提供的成员相同的条件,如以下子项中所述。

16.5.3.5.2 主构造函数

与记录类一样,具有 delimited_parameter_list 的记录结构具有实现提供的主要构造函数(§15.16.4.6.2)。 记录类版本的语义也在此处应用,并由此子句中的文本进行扩充。

对于记录类,实现应为每个参数提供一个专用的仅初始化字段。 但是,对于记录结构,存储是读写的,并以某种未指定的方式提供。

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

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

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

对于与显式声明的实例字段具有相同名称和类型的 delimited_parameter_list 的每个参数,此子句的其余部分不适用。

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

对于记录结构:

  • 如果记录结构具有修饰符,则创建公共属性和自动属性,否则为 />。 这两种类型的集访问器(setinit)都被视为“匹配”。 因此,用户可以声明仅初始化属性来代替提供的可变属性。

  • 重写具有匹配类型的继承 abstract 属性。

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

  • 如果继承的属性没有 publicgetset/init 访问器,则这是一个错误。

  • 如果继承的属性或字段处于隐藏状态,则为错误。

  • 自动属性初始化为相应的主构造函数参数的值。

  • 属性可以分别应用于property:field:提供的自动属性及其后盾字段,以语法方式应用于相应的记录结构参数的属性。

16.5.3.5.4 析构

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

16.6 内联数组

用属性 System.Runtime.CompilerServices.InlineArrayAttribute 修饰的结构类型(§23.5.13)是 内联数组类型,它是托管类型。 该类型的实例是 一个内联数组,一个结构,包含相同类型的给定数量的元素的连续块,而没有其他元素。 它是不安全代码固定大小的缓冲区(§24.8)的安全代码等效项。

由于存在一些限制(如下所述),内联数组可以像数组一样使用(§17)。

考虑以下情况:

var buffer = new Buffer();

for (int i = 0; i < 5; ++i)
{
    buffer[i] = i;
}

foreach (var i in buffer)
{
    Console.WriteLine(i);
}

Console.WriteLine($"buffer[^2]        = >{buffer[^2]}<");
Console.WriteLine($"buffer[1..^0][0]  = >{buffer[1..^0][0]}<");

[System.Runtime.CompilerServices.InlineArray(5)]
public struct Buffer
{
    private int _element0;
    // other, optional, non-instance-field members 
}

这会生成以下输出:

0
1
2
3
4
buffer[^2]        = >3<
buffer[1..^0][0]  = >1<

属性构造函数需要一个正值 int 参数,该参数指示从此类型创建的每个内联数组中的元素数,在本例中为 5。 结构类型应准确声明一个实例字段,其类型将成为内联数组元素的实例字段。 实例字段的可访问性不会影响程序通过其结构实例名称访问内联数组的能力,在本例中。 buffer

实例字段声明存在许多限制:

  • 它不得包含field_modifierreadonlyrefrequiredvolatile
  • 它不应声明为 fixed_size_buffer_declaration§24.8)。
  • type 应为作为类型参数有效的类型。

buffer是一个实例Buffer,包含一个由五int个元素占用的内存buffer._element0相同的数组buffer[0]。 但是,与具有公共 Length 属性的数组类型不同, buffer 它不派生自 System.Array,并且没有此类成员。

注意:可以将公共Length属性添加到(返回sizeof(Buffer)/sizeof(int)该属性Buffer,但该属性必须是不安全的成员)。 或者,可以对其属性执行隐式转换 System.Span<int> 或使用 System.ReadOnlySpan<int>Length 属性。 尾注

内联数组是集合;因此,它可以由 foreach 语句迭代,如下所示。

可以通过下标(§12.8.12.3)访问内联数组的元素进行读取或写入。

列表模式(§11.2.11)不应在内联数组的上下文中使用。

内联数组类型是集合文本的有效可构造集合目标类型。

对于内联数组类型声明的任何索引器或 Slice 方法,其签名与为非内联数组处理定义的签名匹配,不得在内联数组元素访问期间使用。

示例:请考虑以下事项:

var buffer = new Buffer();
int x = buffer[2];      // element access

[System.Runtime.CompilerServices.InlineArray(5)]
public struct Buffer
{
    private int _element0;
    public int this[int index] // NOT USED for element access 
    {
        get
        {
            …
        }
    }
}

即使结构声明了采用 int 参数的索引器,但元素访问 buffer[2]不使用索引器。 示例结束

16.7 记录结构和非记录结构差异

记录结构与非记录结构在几种重要方面不同:

  • 它使用关键字 record struct 而不是只是 struct声明的。
  • 它具有实现为它提供的多个成员。
  • 其声明可能包含具有零个或多个参数 的delimited_parameter_list ,这会导致预配具有这些参数的主构造函数。 对于每个参数,实现应提供类似于字段的存储和具有 get 和 init 访问器的属性。 还提供了一种方法 Deconstruct
  • 可以省略其记录结构正文。
  • 它不应有一 Clone个名为的成员。
  • 它不应具有具有不安全类型的实例字段。

16.8 类和结构差异

16.8.1 常规

结构在几个重要方面与类不同:

  • 结构是值类型(§16.8.2)。
  • 所有结构类型都隐式继承自类 System.ValueType§16.8.3)。
  • 对结构类型的变量的赋值将创建要分配的值 的副本§16.8.4)。
  • 结构的默认值是通过将所有字段设置为其默认值(§16.8.5)生成的值。
  • 装箱和取消装箱操作用于在结构类型和某些引用类型(§16.8.6)之间进行转换。
  • 结构成员(§16.8.7)中的含义this不同。
  • 不允许结构声明终结器。
  • 允许事件声明、属性声明、属性访问器、索引器声明和方法声明具有修饰符 readonly ,而类中这些成员类型通常不允许该修饰符。

16.8.2 值语义

结构是值类型(§8.3),据说具有值语义。 另一方面,类是引用类型(§8.2),据说具有引用语义。

结构类型的变量直接包含结构的数据,而类类型的变量包含对包含数据的对象的引用。 当结构 B 包含类型为 A 的实例字段,并且 A 是一个结构类型时,若 A 依赖于 B 或从 B 中构造的类型,将导致编译时错误。 如果 X 包含类型 的实例字段,则结构 YX。 鉴于此定义,一个结构所依赖的完整结构集合是直接依赖关系的传递闭包。

示例:

struct Node
{
    int data;
    Node next; // error, Node directly depends on itself
}

是一个错误,因为 Node 包含其自己的类型的实例字段。 另一个示例

struct A { B b; }
struct B { C c; }
struct C { A a; }

是一个错误,因为每种类型ABC都相互依赖。

示例结束

使用类时,两个变量可以引用同一对象,因此,一个变量上的操作可能会影响另一个变量引用的对象。 使用结构时,变量都有其自己的数据副本(除了按引用参数的情况除外),并且无法对一个参数执行操作来影响另一个参数。 此外,除了显式可为 null (§8.3.12) 时,结构类型的值不可能为 null

注意:如果结构包含引用类型的字段,或者该字段是引用变量,则其他操作可以更改引用的对象的内容。 但是,字段本身的值(即它引用的对象)不能通过不同结构值的突变来更改。 尾注

示例:给定以下内容

struct Point
{
    public int x, y;

    public Point(int x, int y) 
    {
        this.x = x;
        this.y = y;
    }
}

class A
{
    static void Main()
    {
        Point a = new Point(10, 10);
        Point b = a;
        a.x = 100;
        Console.WriteLine(b.x);
    }
}

输出为 10. 将a赋值给b会创建一个值的副本,因此b不受将其赋值给a.x的影响。 相反,如果 Point 声明为类,输出将是 100 因为 ab 引用相同的对象。

示例结束

16.8.3 继承

所有结构类型都隐式继承自类,而类 System.ValueType又继承自类 object。 结构声明可以指定已实现接口的列表,但结构声明不可能指定基类。

结构类型从不抽象,始终隐式密封。 abstractsealed 修饰符因此在结构声明中不被允许使用。

由于结构不支持继承,因此结构成员的声明可访问性不能 protected为, private protected或者 protected internal

结构中的函数成员不能是抽象或虚拟的,并且 override 仅允许修饰符重写继承自 System.ValueType的方法。

16.8.4 赋值

对结构类型的变量的赋值将创建 要分配的值的副本 。 这不同于对类类型的变量的赋值,该变量复制引用而不是引用标识的对象。

与赋值类似,当结构作为值参数传递或作为函数成员的结果返回时,将创建结构的副本。 可以使用按引用参数将结构按引用传递给函数成员。

当结构的属性或索引器是赋值的目标时,与属性或索引器访问关联的实例表达式应归类为变量。 如果实例表达式被归类为值,则会发生编译时错误。 在 §12.24.2 中对此进行了进一步详细介绍。

16.8.5 默认值

§9.3 中所述,某些类型的变量在创建时会自动初始化为其默认值。 对于类类型和其他引用类型的变量以及引用变量字段,此默认值为 null。 但是,由于结构是不能 null的值类型,因此结构的默认值是通过将所有值类型字段设置为其默认值以及所有引用变量字段和引用类型字段 null生成的值。

示例:引用上面声明的 Point 结构,此示例

Point[] a = new Point[100];

将数组中的每个Point初始化为通过将xy字段设置为零所产生的值。

示例结束

结构的默认值对应于结构的默认构造函数返回的值(§8.3.3)。 当结构不声明显式无参数实例构造函数时,将合成默认构造函数,并且始终返回从将所有字段设置为其默认值的结果的值。 即使结构声明显式无参数实例构造函数(§16.8.9),表达式default也始终生成零初始化的默认值。

注意:结构应设计为将默认初始化状态视为有效状态。 在示例中

struct KeyValuePair
{
    string key;
    string value;

    public KeyValuePair(string key, string value)
    {
        if (key == null || value == null)
        {
            throw new ArgumentException();
        }

        this.key = key;
        this.value = value;
    }
}

用户定义的实例构造函数仅可在显式调用时保护其免受 null 值侵害。 如果KeyValuePair变量进行默认值初始化,则key字段和value字段将null,并且结构体应准备好处理此状态。

尾注

16.8.6 装箱和取消装箱

类类型的值可以转换为类型 object ,也可以转换为类实现的接口类型,只需在编译时将引用视为另一种类型即可。 同样,可以在不更改引用的情况下将类型 object 或接口类型的值转换为类类型(但在这种情况下,需要运行时类型检查)。

由于结构不是引用类型,因此对于结构类型,这些操作以不同的方式实现。 当结构类型的值转换为某些引用类型(如在 §10.2.9 中定义)时,将执行装箱操作。 同样,当某些引用类型的值(如在 §10.3.7 中定义)转换回结构类型时,将执行取消装箱操作。 与类类型上的相同操作的一个主要区别是,装箱和取消装箱操作会将结构值复制到装箱实例中或从装箱实例中复制出来。

注意:因此,执行装箱或拆箱操作后,对拆箱的 struct 所做的更改不会在装箱的 struct 中反映。 尾注

有关装箱和取消装箱的更多详细信息,请参阅 §10.2.9§10.3.7

16.8.7 此含义

结构中的含义this与类中的含义this不同,如 §12.8.14 中所述。 当结构类型替代从 System.ValueType(例如 EqualsGetHashCodeToString)继承的虚拟方法时,通过结构类型的实例调用该虚拟方法不会导致发生装箱。 即使结构用作类型参数,并且调用通过类型参数类型的实例进行,也是如此。

示例:

struct Counter
{
    int value;
    public override string ToString() 
    {
        value++;
        return value.ToString();
    }
}

class Program
{
    static void Test<T>() where T : new()
    {
        T x = new T();
        Console.WriteLine(x.ToString());
        Console.WriteLine(x.ToString());
        Console.WriteLine(x.ToString());
    }

    static void Main() => Test<Counter>();
}

程序的输出为:

1
2
3

尽管 ToString 有副作用是一种不好的做法,但该示例表明在三次调用 x.ToString() 时都没有发生装箱。

示例结束

同样,在值类型中实现成员时,访问约束类型参数上的成员时永远不会隐式发生装箱。 例如,假设接口 ICounter 包含一个方法 Increment,该方法可用于修改值。 如果使用 ICounter 作为约束,则将使用对调用 Increment 所针对的变量的引用(而绝不是装箱副本)来调用 Increment 方法的实现。

示例:

interface ICounter
{
    void Increment();
}

struct Counter : ICounter
{
    int value;

    public override string ToString() => value.ToString();

    void ICounter.Increment() => value++;
}

class Program
{
    static void Test<T>() where T : ICounter, new()
    {
        T x = new T();
        Console.WriteLine(x);
        x.Increment();              // Modify x
        Console.WriteLine(x);
        ((ICounter)x).Increment();  // Modify boxed copy of x
        Console.WriteLine(x);
    }

    static void Main() => Test<Counter>();
}

第一次调用修改 Increment 变量 x中的值。 这与第二次调用 Increment 不等效,后者修改了 x 装箱副本中的值。 因此,程序的输出为:

0
1
1

示例结束

16.8.8 字段

16.8.8.1 字段初始值设定项

§16.8.5 中所述,结构的默认值由将所有值类型和引用变量字段设置为其默认值以及所有引用类型字段 null的结果组成。 允许结构静态字段和实例字段包括变量初始值设定项;但是,在实例字段初始值设定项的情况下,至少应声明一个实例构造函数,或者对于记录结构,应存在 delimited_parameter_list

示例:

Console.WriteLine($"Point is {new Point()}");

struct Point
{
    public int x = 1;
    public int y = 1;

    public Point() { }

    public override string ToString()
    {
        return "(" + x + ", " + y + ")";
    }
}
Point is (1, 1)

示例结束

当结构实例构造函数没有构造函数初始值设定项时,该构造函数将隐式执行由在其结构中声明的实例字段 variable_initializer指定的初始化。 这对应于在进入构造函数时立即执行的一系列赋值。

当结构实例构造函数具有 this() 表示默认无参数构造函数的构造函数初始值设定项时,声明的构造函数会隐式清除所有实例字段,并执行由在其结构中声明的实例字段 的 variable_initializer指定的初始化。 当进入构造函数时,所有值类型字段都设置为其默认值,并且所有引用类型字段都设置为 null。 紧接着,将执行与 variable_initializer对应的一系列赋值。

直接在具有struct_modifierstruct_declaration内声明的field_declaration应具有readonly

16.8.8.2 Ref 字段

struct_field_declaration
    : attributes? field_modifier* ('readonly'? 'ref' 'readonly'?)? type
      variable_declarators ';'
    ;

field_modifier在 §15.5.1 中介绍。

refstruct_field_declaration,readonly refref readonly§15.5 中所述。

refreadonly ref字段是引用变量,应仅在结构中ref声明。

请考虑以下 ref 结构声明:

ref struct RwS
{
    public static int rwField = 100;

    public ref int                   rwRefToRwData = ref rwField;
    public ref readonly int          rwRefToRoData = ref rwField;
    public readonly ref int          roRefToRwData = ref rwField;
    public readonly ref readonly int roRefToRoData = ref rwField;
    public RwS() { /*…*/ }
}

rwRefToRwData 是可写引用变量,其引用被视为可 int写。 rwRefToRoData 是可写引用变量,其引用被视为只读 introRefToRwData 是只读引用变量,其引用被视为可 int写变量。 roRefToRoData 是只读引用变量,其引用被视为只读 int。 可以直接写入读/写字段 rwField ,并通过引用变量 rwRefToRwDataroRefToRwData

只读引用变量可以通过初始值设定项或构造函数内部的赋值或仅限初始化的 setter 获取值。

请考虑以下只读 ref 结构声明:

readonly ref struct RoS
{
    public static int rwField = 200;

    public readonly ref int          roRefToRwData = ref rwField;
    public readonly ref readonly int roRefToRoData = ref rwField;
    public RoS() { /*...*/ }
}

roRefToRwData 是只读引用变量,其引用被视为可 int写变量。 roRefToRoData 是只读引用变量,其引用被视为只读 int。 可以直接写入读/写字段 rwField ,并通过引用变量 roRefToRwData进行写入。

16.8.9 构造函数

结构可以声明具有零个或多个参数的实例构造函数。 如果结构没有显式声明的无参数实例构造函数,则使用公共辅助功能合成一个结构,该构造函数始终返回将所有值类型字段设置为其默认值、所有引用变量字段到 null 引用的结果,并将所有引用类型字段设置为 null§8.3.3.3)。 在这种情况下,当该构造函数执行时,将忽略任何实例字段初始值设定项。

显式声明的无参数实例构造函数应具有公共辅助功能。

示例:给定以下内容:

using System;
struct Point
{
    int x = -1, y = -2;

    public Point(int x, int y) 
    {
        this.x = x;
        this.y = y;
    }

    public override string ToString()
    {
        return "(" + x + ", " + y + ")";
    }
}

class A
{
    static void Main()
    {
        Console.WriteLine($"Point is {new Point()}");
        Console.WriteLine($"Point is {new Point(0,0)}");
    }
}
Point is (0, 0)
Point is (0, 0)

这些语句都创建一个 Point 具有 xy 初始化为零的语句,在调用无参数实例构造函数时,这可能令人吃惊,因为两个实例字段都有初始值设定项,但它们 不会 执行。

示例结束

不允许结构实例构造函数包含表单 base(argument_list)的构造函数初始值设定项,其中 argument_list 是可选的。 实例构造函数的执行不应导致构造函数在结构的基类型 System.ValueType中执行。

this结构实例构造函数的参数对应于结构类型的输出参数。 因此, this 应在构造函数返回的每个位置明确分配 (§9.4)。 同样,在明确赋值之前,不能在构造函数主体中读取它(即使是隐式读取也不例外)。

如果结构实例构造函数指定了构造函数初始值设定项,则该初始值设定项被视为在构造函数正文之前发生的对 this 的明确赋值。 因此,正文本身没有初始化要求。

实例字段(非 fixed 字段)应在没有 this() 初始值设定项的结构实例构造函数中明确分配。

示例:请考虑以下实例构造函数实现:

struct Point
{
    int x, y;

    public int X
    {
        set { x = value; }
    }

    public int Y 
    {
        set { y = value; }
    }

    public Point(int x, int y) 
    {
        X = x; // error, this is not yet definitely assigned
        Y = y; // error, this is not yet definitely assigned
    }
}

任何实例函数成员(包括属性XY的 set 访问器)在构造的结构体的所有字段被明确分配之前,不能被调用。 但是,请注意,如果 Point 类而不是结构,则允许实例构造函数实现。 这有一个例外,涉及自动实现的属性(§15.7.4)。 明确赋值规则(§12.24.2)特别免除在该结构类型的实例构造函数中对结构类型的自动属性的赋值:此类赋值被视为自动属性的隐藏后盾字段的明确赋值。 因此,允许执行以下操作:

struct Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point(int x, int y)
    {
        X = x; // allowed, definitely assigns backing field
        Y = y; // allowed, definitely assigns backing field
   }
}

结束示例]

16.8.10 静态构造函数

结构的静态构造函数遵循与类相同的大多数规则。 结构类型的静态构造函数的执行由应用程序域中发生的以下事件中的第一个触发:

  • 引用了结构类型的静态成员。
  • 调用了结构类型的显式声明的构造函数。

注意:创建结构类型的默认值(§16.8.5)不会触发静态构造函数。 (例如,这是数组中元素的初始值。 end note

16.8.11 属性

struct_declaration中实例属性的property_declaration§15.7.1)可能包含property_modifierreadonly。 但是,静态属性不应包含该修饰符。

尝试通过该结构中声明的只读属性修改实例结构变量的状态是编译时错误。

自动实现的属性如果有 `readonly` 修饰符,同时也有 `set` 存取器,则存在编译时错误。

如果结构体中自动实现的属性具有readonly访问器,将产生编译时错误。

在结构体内 readonly 声明的自动属性不需要 readonly 修饰符,因为其 get 访问器被默认假设为只读。

在属性本身和其readonly访问器或get访问器上有set修饰符都是编译时错误。

如果一个属性的所有访问器都有只读修饰符,那就会导致编译时错误。

注意:若要更正错误,请将修饰符从访问器移动到属性本身。 尾注

对于属性访问器表达式, s.P

  • 如果在 s.PM 中的进程将创建临时副本T时调用类型的 set 访问器s,则为编译时错误。
  • 如果 s.P 调用类型的 Tget 访问器,则遵循 §12.6.6.1 中的过程,包括根据需要创建临时副本 s

自动实现的属性(§15.7.4)使用隐藏的后盾字段,这些字段只能访问属性访问器。

注意:此访问限制意味着包含自动实现属性的结构中的构造函数通常需要一个显式的构造函数初始值设定项,即使在其他情况下不需要,以确保在调用任何函数成员或构造函数返回之前,所有字段都被明确地分配。 尾注

16.8.12 方法

struct_declaration中的实例方法的method_declaration§15.6.1)可能包含method_modifier。 但是,静态方法不应包含该修饰符。

尝试通过该结构中声明的只读方法修改实例结构变量的状态是编译时错误。

尽管只读方法可以调用兄弟方法中的非只读方法、属性或索引器的get访问器,但这样做会作为一种防御措施导致隐式创建this的副本。

只读方法可以调用只读的同级属性或索引器集访问器。 如果同级成员的访问器不是显式或隐式读取的,则会发生编译错误。

分部方法的所有 method_declaration应具有修饰符,或者其中任何一个 readonly 都不得具有修饰符。

16.8.13 索引器

struct_declaration中实例索引器的indexer_declaration§15.9)可能包含indexer_modifierreadonly

尝试通过在该结构中声明的只读索引器修改实例结构变量的状态是编译时错误。

对索引器本身以及其readonlyget访问器具有set修饰符是编译时错误。

索引器如果其所有访问器都包含只读修饰符,这将导致编译时错误。

注意:若要更正错误,请将修饰符从访问器移到索引器本身。 尾注

16.8.14 事件

实例的 event_declaration§15.8.1)中非字段式 struct_declaration 事件可能包含 event_modifierreadonly。 但是,静态事件不应包含该修饰符。

16.8.15 安全上下文约束

16.8.15.1 常规

在编译时,每个表达式都与一个上下文相关联,在该上下文中可以安全地访问该实例及其所有字段,以及其安全上下文 安全上下文是一个包含表达式且值可以安全转义到的上下文。

编译时类型不是引用结构的任何表达式都具有调用方上下文的安全上下文。

任何类型的 default 表达式都具有调用方上下文的安全上下文。

对于编译时类型为 ref 结构的任何非默认表达式,其安全上下文由以下部分定义。

安全上下文记录一个值可以复制到哪个上下文中。 如果将具有安全上下文 E1 的表达式 S1 分配给具有安全上下文 E2 的表达式 S2,并且 S2 上下文比 S1 上下文更宽,则会出现错误。

有四个不同的安全上下文值,与为引用变量(§9.7.2)定义的 ref-safe-context 值相同: declaration-blockfunction-memberreturn-onlycaller-context。 表达式的安全上下文限制其用法,如下所示:

  • 对于返回语句 return e1,安全上下文 e1 应至少为仅返回。
  • 对于赋值 e1 = e2,安全上下文 e2 至少应具有与安全上下文 e1 相同的广度。
  • 对于对 out 参数的赋值,右侧的安全上下文至少应仅返回。

16.8.15.2 参数安全上下文

引用结构类型的参数(包括实例方法的 this 参数)具有调用方上下文的安全上下文。

out ref 结构类型的参数具有仅返回的安全上下文。

this结构构造函数中的参数具有仅返回的安全上下文。

16.8.15.3 局部变量安全上下文

ref 结构类型的局部变量具有安全上下文,如下所示:

  • 如果变量是循环的 foreach 迭代变量,则变量的安全上下文与循环表达式的安全上下文 foreach 相同。
  • 否则,如果变量的声明具有初始值设定项,则变量的安全上下文与该初始值设定项的安全上下文相同。
  • 否则,该变量在声明时未初始化,并且具有调用者上下文的安全上下文。

请参阅 §9.7.2.1§9.7.2.2。

16.8.15.4 现场安全上下文

对字段e.F的引用,其中F的类型为 ref 结构类型,其安全上下文与e的安全上下文相同。

16.8.15.5 运算符

用户定义的运算符的应用程序被视为方法调用(§16.8.15.6)。

对于生成值(例如 e1 + e2c ? e1 : e2)的运算符,结果的安全上下文是运算符操作数的安全上下文中最窄的上下文。 因此,对于生成值的一元运算符,例如 +e,结果的安全上下文是操作数的安全上下文。

注意:条件运算符的第一个操作数是一个 bool,因此其安全上下文为调用方上下文。 由此可见,所得的安全上下文是第二和第三个操作数的最窄安全上下文。 尾注

16.8.15.6 方法和属性调用

由方法调用 e1.M(e2, ...) 或属性调用 e.P生成的值(其中 M() 不返回 ref-to-ref-struct)具有以下上下文中最小的安全上下文:

  • caller-context。
  • 当返回为一个 ref struct时,由所有参数表达式(包括接收方)提供的安全上下文,不包括与 scoped 参数和排除 out 参数对应的参数。
  • 当返回为一个 ref struct时,由所有 ref 参数提供的 ref-safe-context,不包括与参数对应的 scoped ref 参数和排除 out 参数。

如果 M() 确实返回 ref-to-ref-struct,则 safe-context 与 ref-ref-struct 的所有参数的安全上下文相同。 如果有多个具有不同安全上下文的此类参数,则会出现错误。

出于这些规则的目的,传递给参数expr的给定参数p

  1. p如果是scoped ref,则expr不提供 ref-safe-context。
  2. p如果是scoped,则expr不提供安全上下文。
  3. p如果是out,则expr不提供 ref-safe-context 或 safe-context。

属性调用(无论是get还是set)根据上述规则,被视为调用其底层方法的方法。

示例:下面说明了如何 scoped 影响方法的返回值的安全上下文:

ref struct RS
{
    public ref int RefField;
    public RS(ref int i) { RefField = ref i; }
}

class C
{
    static RS CreateAndCapture(ref int value)
    {
        // OK: ref-safe-context of `ref value` is caller-context,
        // safe-context contributed is caller-context.
        return new RS(ref value);
    }

    static RS CreateWithoutCapture(scoped ref int value)
    {
        // Error: `value` is scoped ref so it does not contribute
        // ref-safe-context. The constructor needs ref-safe-context
        // of caller-context but `value` only has function-member.
        return new RS(ref value);
    }
}

示例结束

16.8.15.7 方法参数必须匹配

对于任何方法调用 e.M(a1, a2, ... aN)

  1. 从以下位置计算最窄的安全上下文:

    • 调用方上下文。
    • 所有参数的安全上下文。
    • 其相应参数具有 ref 调用方上下文的 ref-safe-context 的所有参数的 ref-safe-context。
  2. 类型的所有 ref 参数 ref struct 应由具有该安全上下文的值分配。 在此规则中, ref 通用化为包含 inout

对于任何方法调用 e.M(a1, a2, ... aN)

  1. 从以下位置计算最窄的安全上下文:

    • 调用方上下文。
    • 所有参数的安全上下文。
    • 其相应参数不是ref的所有scoped参数的 ref-safe-context。
  2. 类型的所有 out 参数 ref struct 应由具有该安全上下文的值分配。

scoped 的存在允许开发人员通过将不返回的参数标记为 scoped 来减少此规则产生的摩擦。 这在上述两种情况下都从(1)中删除了其参数,并为调用方提供了更大的灵活性。

示例:下面说明了 method-arguments-must-match 规则如何防止具有较窄安全上下文的值存储在 ref 具有更广泛的安全上下文的参数中:

ref struct R { }

class C
{
    static void F0(ref R a, scoped ref R b) { }

    static void F1(ref R x, scoped R y)
    {
        // Error: The narrowest safe-context is function-member (from `y`)
        // but `x` is a ref argument of a ref struct type whose
        // safe-context is caller-context. It must be assignable by
        // a value with that narrowest safe-context, which fails.
        F0(ref x, ref y);
    }
}

示例结束

16.8.15.8 声明表达式的推断安全上下文

声明变量(out)或解构(M(x, out var y)(var x, var y) = M())的安全上下文是以下最窄的一个:

  • 调用方上下文。
  • 如果标记 scoped了 out 变量,则声明块(即 function-member 或 narrower)。
  • 如果 out 变量的类型是一个 ref struct,请考虑包含调用的所有参数,包括接收方:
    • 任何参数的安全上下文,其中其对应的参数不是 out ,并且具有仅返回或更宽的安全上下文。
    • 其相应参数的 ref-safe-context 具有仅返回或更宽的 ref-safe-context。

示例:下面说明了声明变量的安全 out 上下文如何从其他参数推断到调用:

ref struct RS
{
    public RS(ref int x) { }

    static void M0(RS input, out RS output) => output = input;

    static RS M1()
    {
        var i = 0;
        var rs1 = new RS(ref i);  // safe-context of rs1 is function-member
        M0(rs1, out var rs2);     // safe-context of rs2 is function-member
        return rs2;               // Error: rs2 cannot escape function-member
    }

    static void M2(RS rs1)
    {
        M0(rs1, out scoped var rs2); // scoped forces safe-context to
                                     // declaration-block
    }
}

M1中,安全上下文rs2调用方上下文最窄的上下文,也是函数成员(rs1)的安全上下文。 因此 rs2 ,无法返回。 在M2中,scoped修饰符强制声明rs2的安全上下文。

示例结束

16.8.15.9 对象初始值设定项安全上下文

对象初始值设定项表达式的安全上下文是最窄的:

  1. 构造函数调用的安全上下文。
  2. 可转义到接收方的成员初始值设定项索引器的安全上下文和 ref-safe-context。
  3. 成员初始值设定项中分配给非只读 setter 的 RHS 的安全上下文,或者在 ref 赋值的情况下为 ref-safe-context。

注意:另一种建模方式是将任何参数视为构造函数的参数分配给接收方的成员初始值设定项。 尾注

示例:下面说明了对象初始值设定项如何缩小生成的值的安全上下文:

using System;

ref struct S
{
    public Span<int> Field;
    public S(ref int i) { }
}

class C
{
    static S Example()
    {
        Span<int> stackSpan = stackalloc int[42];
        int i = 0;

        // safe-context is narrowest of:
        //   constructor safe-context (caller-context) and
        //   RHS of Field assignment (stackSpan: function-member)
        // = function-member
        var x = new S(ref i) { Field = stackSpan };
        return x; // Error: x has safe-context of function-member
    }
}

示例结束

16.8.15.10 stackalloc

stackalloc 表达式的结果具有函数成员的安全上下文。

16.8.15.11 构造函数调用

new调用构造函数的表达式遵循与方法调用相同的规则,该方法调用被视为返回正在构造的类型。

此外,如果存在任何初始值设定项,则安全上下文是所有对象初始值设定项表达式的所有参数和操作数的最小安全上下文。 有关详细信息,请参阅 §16.8.15.9