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
    : attributes? struct_modifier* 'ref'? 'partial'? 'struct'
      identifier type_parameter_list? struct_interfaces?
      type_parameter_constraints_clause* struct_body ';'?
    ;

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

record_struct_body
    : struct_body ';'?
    | ';'
    ;

struct_declaration适用于非记录结构记录结构

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

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

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

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

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

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

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

delimited_parameter_list中的参数不得具有refoutthis修饰符;但是,in允许修饰params符。 对于record_struct_declaration,record_struct_bodys{}{};,并且;等效。 它们都表示唯一的成员是由编译器合成的成员(§16.4)。

16.2.2 结构修饰符

struct_declaration 可以选择包括一系列 struct_modifier

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

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

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

除了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.5.15)复制出来。 用于确定 ref 结构的安全上下文的规则在 §16.5.15 中介绍。

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

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

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

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

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

这些约束可确保类型的 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* '}'
    ;

16.3 结构成员

16.3.1 常规

结构的成员包括由其 struct_member_declaration引入的成员和从类型 System.ValueType继承的成员。 对于记录结构,成员集还包括编译器生成的合成成员(§合成器成员)。

struct_member_declaration
    : constant_declaration
    | 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)中可用。

注意:所有种类的 class_member_declarationfinalizer_declaration 除外)还全都是 struct_member_declaration尾注

除了 §16.5 中所述的差异外,§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 合成记录结构成员

16.4.1 常规

对于记录结构,除非 在record_struct_body 中声明具有“匹配”签名的成员或继承具有“匹配”签名的可访问的具体非虚拟成员,否则将合成成员。 如果两个成员具有相同的签名,或被视为在继承方案中“隐藏”,则两个成员被视为匹配。 (请参阅签名和重载 §7.6

合成成员在以下子项中进行了介绍。

16.4.2 相等成员

合成的相等成员类似于记录类(§15.16.2),但缺少 EqualityContractnull 检查或继承。

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

public readonly bool Equals(R other);

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

如果用户 Equals(R other) 定义(即未合成),但 GetHashCode 未合成,将生成警告。

如果记录结构中每个实例字段fieldN的值(其中字段TN类型为true)的值System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN),则合成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();

此方法可以显式声明。

如果其中一个和GetHashCode()显式声明,但另一Equals(R)种方法不是,则应报告警告。

合成重写GetHashCode()应返回int将每个实例字段fieldN的值System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)TN类型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.4.3 打印成员

记录结构包括等效于以下内容的合成方法:

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.4.4 位置记录结构成员

16.4.4.1 常规

除了提供上述子项中描述的成员外,位置记录结构(§16.2.1)还合成了与其他成员相同的条件的其他成员,如以下子集中所述。

16.4.4.2 主构造函数

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

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

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

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

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

主构造函数的参数以及记录结构的成员位于实例字段或属性的初始值设定项范围内。 实例成员在这些位置将是一个错误,但主构造函数的参数将位于范围和可用范围内,并且会隐藏成员。 静态成员也可用。

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

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

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.4.4.3 属性

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

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

对于记录结构:

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

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

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

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

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

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

  • 可以使用或目标将属性应用于合成的自动属性及其后盾字段 property: ,这些 field: 属性在语法上应用于相应的记录结构参数。

16.4.4.4 解构

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

16.5 类和结构差异

16.5.1 常规

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

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

16.5.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.5.3 继承

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

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

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

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

16.5.4 赋值

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

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

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

16.5.5 默认值

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

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

Point[] a = new Point[100];

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

示例结束

结构的默认值对应于结构的默认构造函数返回的值(§8.3.3)。 当结构不声明显式无参数实例构造函数时,将合成默认构造函数,并且始终返回从将所有字段设置为其默认值的结果的值。 即使结构声明显式无参数实例构造函数(§16.4.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.5.6 装箱和取消装箱

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

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

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

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

16.5.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.5.8 字段初始值设定项

§16.5.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.5.9 构造函数

结构可以声明具有零个或多个参数的实例构造函数。 如果结构没有显式声明的无参数实例构造函数,则使用公共辅助功能合成一个结构,该构造函数始终将所有值类型字段设置为其默认值,并将所有引用类型字段都返回的值(null§8.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.5.10 静态构造函数

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

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

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

16.5.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.5.12 方法

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

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

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

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

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

16.5.13 索引器

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

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

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

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

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

16.5.14 事件

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

16.5.15 安全上下文约束

16.5.15.1 常规

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

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

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

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

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

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

  • 对于 return 语句 return e1,安全上下文 e1 应为调用方上下文。
  • 对于赋值 e1 = e2,安全上下文 e2 至少应具有与安全上下文 e1 相同的广度。

对于方法调用,如果存在refout参数为ref struct类型(包括接收器,除非类型为readonly),并且具有安全上下文S1,则任何参数(包括接收器)都不能具有比S1更窄的安全上下文。

16.5.15.2 参数安全上下文

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

16.5.15.3 局部变量安全上下文

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

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

16.5.15.4 现场安全上下文

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

16.5.15.5 运算符

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

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

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

16.5.15.6 方法和属性调用

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

  • 调用方上下文。
  • 所有参数表达式(包括接收方)的安全上下文。

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

16.5.15.7 stackalloc

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

16.5.15.8 构造函数调用

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

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

注意:这些规则的前提是 Span<T> 没有以下形式的构造函数:

public Span<T>(ref T p)

这样的构造函数使作为字段使用的 Span<T> 实例和 ref 字段无法区分。 本文档中所述的安全规则取决于 ref 字段不是 C# 或.NET的有效构造。 尾注