16.1 常规
结构类似于类,它们表示可以包含数据成员和函数成员的数据结构。 但是,与类不同,结构是值类型,不需要堆分配。 变量 struct
类型直接包含struct
类型的数据,而类类型的变量包含对该数据的引用,后者称为对象。
注意:结构对于具有值语义的小型数据结构特别有用。 复数、坐标系中的点或字典中的键值对都是结构的典型示例。 这些数据结构的关键在于,它们很少有数据成员,它们不需要使用继承或引用语义,而是可以使用赋值复制值而不是引用的值语义方便地实现它们。 尾注
如§8.3.5中所描述,C# 提供的简单类型,如int
、double
和bool
,实际上都是结构体类型。
16.2 结构声明
16.2.1 概述
struct_declaration是type_declaration(§14.7),用于声明一个新的结构体:
struct_declaration
: attributes? struct_modifier* 'ref'? 'partial'? 'struct'
identifier type_parameter_list? struct_interfaces?
type_parameter_constraints_clause* struct_body ';'?
;
struct_declaration 由一组可选的属性 (§22) 组成,其后面依次跟着以下内容:一组可选的 struct_modifier (§16.2.2)、一个可选的 ref
修饰符 (§16.2.3)、一个 partial 修饰符 (§15.2.7)、关键字 struct
以及命名接口的标识符、一个可选的 type_parameter_list 规范 (§15.2.3)、一个可选的 struct_interfaces specification (§16.2.5)、一个可选的 type_parameter_constraints-clauses 规范 (§15.2.5)、一个可选的 struct_body (§16.2.6) 结构和一个分号(可选)。
结构声明不得提供 type_parameter_constraints_clause,除非它还提供 type_parameter_list。
提供 type_parameter_list 的结构声明是一个泛型结构声明。 此外,嵌套在泛型类声明或泛型结构声明中的任何结构本身都是泛型结构声明,因为应提供包含类型的类型参数来创建构造类型(§8.4)。
包含 ref
关键字的结构声明不应具有 struct_interfaces 部分。
16.2.2 结构修饰符
struct_declaration 可以选择包括一系列 struct_modifier:
struct_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'readonly'
| unsafe_modifier // unsafe code support
;
unsafe_modifier (§23.2)仅在不安全的代码(§23)中可用。
相同的修饰符在结构声明中多次出现,会导致编译时错误。
除了readonly
,结构声明的修饰符与类声明(§15.2.2)的含义相同。
readonly
修饰符指示 struct_declaration 声明了一种实例不可变的类型。
只读结构具有以下约束:
- 其每个实例字段也应声明
readonly
。 - 它不应声明任何类似字段的事件(§15.8.2)。
将只读结构实例传递给方法时,其 this
处理方式类似于输入参数/参数,该参数禁止写入任何实例字段(构造函数除外)。
16.2.3 Ref 修饰符
修饰符 ref
表示 struct_declaration 声明了一种类型,其实例是在执行堆栈上分配的。 这些类型称为 ref 结构 类型。
ref
修饰符声明实例可以包含类似 ref 的字段,不应从其安全上下文(§16.4.15)中复制。 用于确定 ref 结构的安全上下文的规则在 §16.4.15 中介绍。
如果在以下任一上下文中使用 ref 结构类型,则为编译时错误:
- 作为数组的元素类型。
- 作为类或结构中未使用
ref
修饰符的字段的声明类型。 - 被装箱为
System.ValueType
或System.Object
。 - 作为类型参数。
- 作为元组元素的类型。
- 作为异步方法。
- 迭代器。
- 无法从
ref struct
类型转换为类型object
或类型System.ValueType
。 - 不应声明
ref struct
类型以实现任何接口。 - 在
object
或System.ValueType
中声明但未在ref struct
类型中被替代的实例方法不应与通过该ref struct
类型的接收器来调用。 - 不应通过将方法组转换为委托类型来捕获
ref struct
类型的实例方法。 - ref 结构不应由 lambda 表达式或本地函数捕获。
注意: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 中进一步讨论。
接口实现在 §18.6 中进一步讨论。
16.2.6 结构正文
结构struct_body定义结构的成员。
struct_body
: '{' struct_member_declaration* '}'
;
16.3 结构成员
结构的成员包括由其 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(§23.8.2)仅在不安全的代码(§23)中可用。
注意:所有种类的 class_member_declaration(finalizer_declaration 除外)还全都是 struct_member_declaration。 尾注
除了§16.4中所述的差异外,§15.3至§15.12中提供的类成员的说明也适用于结构成员。
16.4 类和结构差异
16.4.1 常规
结构在几个重要方面与类不同:
- 结构是值类型(§16.4.2)。
- 所有结构类型都隐式继承自类
System.ValueType
(§16.4.3)。 - 对结构类型的变量的赋值将创建 要分配的值的副本 (§16.4.4)。
- 结构的默认值是通过将所有字段设置为其默认值(§16.4.5)生成的值。
- 装箱和取消装箱操作用于在结构类型和某些引用类型(§16.4.6)之间转换。
- 结构成员中的含义
this
不同(§16.4.7)。 - 不允许结构实例字段声明包括变量初始值设定项(§16.4.8)。
- 不允许结构声明无参数实例构造函数(§16.4.9)。
- 不允许结构声明终结器。
- 允许事件声明、属性声明、属性访问器、索引器声明和方法声明具有修饰符
readonly
,而类中这些成员类型通常不允许该修饰符。
16.4.2 值语义
结构是值类型(§8.3),据说具有值语义。 另一方面,类是引用类型(§8.2),据说具有引用语义。
结构类型的变量直接包含结构的数据,而类类型的变量包含对包含数据的对象的引用。 当结构 B
包含类型为 A
的实例字段,并且 A
是一个结构类型时,若 A
依赖于 B
或从 B
中构造的类型,将导致编译时错误。 如果 X
包含类型 的实例字段,则结构 Y
X
。 鉴于此定义,一个结构所依赖的完整结构集合是直接依赖关系的传递闭包。
示例:
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; }
是一个错误,因为每种类型
A
、B
和C
都相互依赖。示例结束
使用类时,两个变量可以引用同一对象,因此,一个变量上的操作可能会影响另一个变量引用的对象。 使用结构时,变量都有其自己的数据副本(除了按引用参数的情况除外),并且无法对一个参数执行操作来影响另一个参数。 此外,除了显式可为 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
因为a
并b
引用相同的对象。示例结束
16.4.3 继承
所有结构类型都隐式继承自类,而类 System.ValueType
又继承自类 object
。 结构声明可以指定已实现接口的列表,但结构声明不可能指定基类。
结构类型从不抽象,始终隐式密封。
abstract
和sealed
修饰符因此在结构声明中不被允许使用。
由于结构不支持继承,因此结构成员的声明可访问性不能 protected
为、 private protected
或 protected internal
。
结构中的函数成员不能是抽象或虚拟的,并且 override
仅允许修饰符重写继承自 System.ValueType
的方法。
16.4.4 转让
对结构类型的变量的赋值将创建 要分配的值的副本 。 这不同于对类类型的变量的赋值,该变量复制引用而不是引用标识的对象。
与赋值类似,当结构作为值参数传递或作为函数成员的结果返回时,将创建结构的副本。 可以使用按引用参数将结构按引用传递给函数成员。
当结构的属性或索引器是赋值的目标时,与属性或索引器访问关联的实例表达式应归类为变量。 如果实例表达式被归类为值,则会发生编译时错误。 §12.21.2 中对此进行了进一步详细介绍。
16.4.5 默认值
如 §9.3 中所述,某些类型的变量在创建时会自动初始化为其默认值。 对于类类型和其他引用类型的变量,此默认值为 null
。 但是,由于结构是不能 null
的值类型,因此结构的默认值是通过将所有值类型字段设置为其默认值和所有引用类型字段 null
生成的值。
示例:引用上面声明的
Point
结构,此示例Point[] a = new Point[100];
将数组中的每个
Point
初始化为通过将x
和y
字段设置为零所产生的值。示例结束
结构的默认值对应于结构的默认构造函数返回的值(§8.3.3)。 与类不同,不允许结构声明无参数实例构造函数。 相反,每个结构都隐式具有无参数实例构造函数,该构造函数始终返回所有字段设置为其默认值的结果。
注意:结构应设计为将默认初始化状态视为有效状态。 在示例中
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.4.6 装箱和取消装箱
类类型的值可以转换为类型 object
,也可以转换为类实现的接口类型,只需在编译时将引用视为另一种类型即可。 同样,可以在不更改引用的情况下将类型 object
或接口类型的值转换为类类型(但在这种情况下,需要运行时类型检查)。
由于结构不是引用类型,因此对于结构类型,这些操作以不同的方式实现。 当结构类型的值转换为某些引用类型(如在 §10.2.9 中定义)时,将执行装箱操作。 同样,当某些引用类型的值(如在 §10.3.7 中定义)转换回结构类型时,将执行取消装箱操作。 与类类型上的相同操作的一个主要区别是,装箱和取消装箱操作会将结构值复制到装箱实例中或从装箱实例中复制出来。
注意:因此,执行装箱或拆箱操作后,对拆箱的
struct
所做的更改不会在装箱的struct
中反映。 尾注
有关装箱和取消装箱的更多详细信息,请参阅 §10.2.9 和 §10.3.7。
16.4.7 这一点的含义
结构中的含义this
与类中的含义this
不同,如 §12.8.14 中所述。 当结构类型替代从 System.ValueType
(例如 Equals
,GetHashCode
或 ToString
)继承的虚拟方法时,通过结构类型的实例调用该虚拟方法不会导致发生装箱。 即使结构用作类型参数,并且调用通过类型参数类型的实例进行,也是如此。
示例:
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.4.8 字段初始化表达式
如§16.4.5 中所述,结构的默认值是将所有值类型字段设置为其默认值,以及所有引用类型字段设置为null
的结果组成。 因此,结构不允许实例字段声明包含变量初始值设定项。 此限制仅适用于实例字段。 结构体的静态字段允许包括变量初始化器。
示例:以下
struct Point { public int x = 1; // Error, initializer not permitted public int y = 1; // Error, initializer not permitted }
出现错误,因为实例字段声明包含变量初始化器。
示例结束
直接在具有struct_modifier的struct_declaration内声明的field_declaration应具有field_modifierreadonly
。
16.4.9 构造函数
与类不同,不允许结构声明无参数实例构造函数。 相反,每个结构都隐式具有无参数实例构造函数,该构造函数始终返回将所有值类型字段设置为其默认值的值,并将所有引用类型字段设置为 null
(§8.3.3)。 结构可以声明具有参数的实例构造函数。
示例:给定以下内容
struct Point { int x, y; public Point(int x, int y) { this.x = x; this.y = y; } } class A { static void Main() { Point p1 = new Point(); Point p2 = new Point(0, 0); } }
这些语句同时创建一个
Point
,并将x
和y
初始化为零。示例结束
不允许结构实例构造函数包含表单 base(
argument_list)
的构造函数初始值设定项,其中 argument_list 是可选的。
this
结构实例构造函数的参数对应于结构类型的输出参数。 因此, this
应在构造函数返回的每个位置明确分配 (§9.4)。 同样,在明确赋值之前,不能在构造函数主体中读取它(即使是隐式读取也不例外)。
如果结构实例构造函数指定了构造函数初始值设定项,则该初始值设定项被视为在构造函数正文之前发生的对 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 } }
任何实例函数成员(包括属性
X
和Y
的 set 访问器)在构造的结构体的所有字段被明确分配之前,不能被调用。 但是,请注意,如果Point
类而不是结构,则允许实例构造函数实现。 这有一个例外,涉及自动实现的属性(§15.7.4)。 明确赋值规则(§12.21.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.4.10 静态构造函数
结构的静态构造函数遵循与类相同的大多数规则。 结构类型的静态构造函数的执行由应用程序域中发生的以下事件中的第一个触发:
- 引用了结构类型的静态成员。
- 调用了结构类型的显式声明的构造函数。
注意:创建结构类型的默认值(§16.4.5)不会触发静态构造函数。 (例如,这是数组中元素的初始值。 end note
16.4.11 属性
struct_declaration中实例属性的property_declaration(§15.7.1)可能包含property_modifierreadonly
。 但是,静态属性不应包含该修饰符。
尝试通过该结构中声明的只读属性修改实例结构变量的状态是编译时错误。
自动实现的属性如果有 `readonly
` 修饰符,同时也有 `set
` 存取器,则存在编译时错误。
如果结构体中自动实现的属性具有set
访问器,将产生编译时错误。
在结构体内 readonly
声明的自动属性不需要 readonly
修饰符,因为其 get
访问器被默认假设为只读。
在属性本身和其get
访问器或set
访问器上有readonly
修饰符都是编译时错误。
如果一个属性的所有访问器都有只读修饰符,那就会导致编译时错误。
注意:若要更正错误,请将修饰符从访问器移动到属性本身。 尾注
自动实现的属性(§15.7.4)使用隐藏的后盾字段,这些字段只能访问属性访问器。
注意:此访问限制意味着包含自动实现属性的结构中的构造函数通常需要一个显式的构造函数初始值设定项,即使在其他情况下不需要,以确保在调用任何函数成员或构造函数返回之前,所有字段都被明确地分配。 尾注
16.4.12 方法
struct_declaration中的实例方法的method_declaration(§15.6.1)可能包含method_modifierreadonly
。 但是,静态方法不应包含该修饰符。
尝试通过该结构中声明的只读方法修改实例结构变量的状态是编译时错误。
尽管只读方法可以调用兄弟方法中的非只读方法、属性或索引器的get访问器,但这样做会作为一种防御措施导致隐式创建this
的副本。
只读方法可以调用只读的同级属性或索引器集访问器。 如果同级成员的访问器不是显式或隐式读取的,则会发生编译错误。
分部方法的所有 method_declaration应具有修饰符,或者其中任何一个 readonly
都不得具有修饰符。
16.4.13 索引器
struct_declaration中实例索引器的indexer_declaration(§15.9)可能包含indexer_modifierreadonly
。
尝试通过在该结构中声明的只读索引器修改实例结构变量的状态是编译时错误。
对索引器本身以及其get
或set
访问器具有readonly
修饰符是编译时错误。
索引器如果其所有访问器都包含只读修饰符,这将导致编译时错误。
注意:若要更正错误,请将修饰符从访问器移到索引器本身。 尾注
16.4.14 事件
实例的 event_declaration (§15.8.1)中非字段式 struct_declaration 事件可能包含 event_modifierreadonly
。 但是,静态事件不应包含该修饰符。
16.4.15 安全上下文约束
16.4.15.1 概述
在编译时,每个表达式都与一个上下文相关联,在该上下文中可以安全地访问该实例及其所有字段,以及其安全上下文。 安全上下文是一个包含表达式且值可以安全转义到的上下文。
编译时类型不是引用结构的任何表达式都具有调用方上下文的安全上下文。
任何类型的 default
表达式都具有调用方上下文的安全上下文。
对于编译时类型为 ref 结构的任何非默认表达式,其安全上下文由以下部分定义。
安全上下文记录一个值可以复制到哪个上下文中。 如果将具有安全上下文 E1
的表达式 S1
分配给具有安全上下文 E2
的表达式 S2
,并且 S2
上下文比 S1
上下文更宽,则会出现错误。
有三个不同的安全上下文值,与为引用变量(§9.7.2)定义的 ref-safe-context 值相同: declaration-block、 function-member 和 caller-context。 表达式的安全上下文限制其用法,如下所示:
- 对于 return 语句
return e1
,安全上下文e1
应为调用方上下文。 - 对于赋值
e1 = e2
,安全上下文e2
至少应具有与安全上下文e1
相同的广度。
对于方法调用,如果存在ref
或out
参数为ref struct
类型(包括接收器,除非类型为readonly
),并且具有安全上下文S1
,则任何参数(包括接收器)都不能具有比S1
更窄的安全上下文。
16.4.15.2 参数安全上下文
引用结构类型的参数(包括实例方法的 this
参数)具有调用方上下文的安全上下文。
16.4.15.3 局部变量安全上下文
ref 结构类型的局部变量具有安全上下文,如下所示:
- 如果变量是循环的
foreach
迭代变量,则变量的安全上下文与循环表达式的安全上下文foreach
相同。 - 否则,如果变量的声明具有初始值设定项,则变量的安全上下文与该初始值设定项的安全上下文相同。
- 否则,该变量在声明时未初始化,并且具有调用者上下文的安全上下文。
16.4.15.4 现场安全上下文
对字段e.F
的引用,其中F
的类型为 ref 结构类型,其安全上下文与e
的安全上下文相同。
16.4.15.5 运算符
用户定义的运算符的应用程序被视为方法调用(§16.4.15.6)。
对于生成值(例如 e1 + e2
或 c ? e1 : e2
)的运算符,结果的安全上下文是运算符操作数的安全上下文中最窄的上下文。 因此,对于生成值的一元运算符,例如 +e
,结果的安全上下文是操作数的安全上下文。
注意:条件运算符的第一个操作数是一个
bool
,因此其安全上下文为调用方上下文。 由此可见,所得的安全上下文是第二和第三个操作数的最窄安全上下文。 尾注
16.4.15.6 方法和属性调用
由方法调用 e1.M(e2, ...)
或属性调用 e.P
生成的值具有以下上下文中最小的安全上下文:
- 调用方上下文。
- 所有参数表达式(包括接收方)的安全上下文。
属性调用(无论是get
还是set
)根据上述规则,被视为调用其底层方法的方法。
16.4.15.7 stackalloc
stackalloc 表达式的结果具有函数成员的安全上下文。
16.4.15.8 构造函数调用
new
调用构造函数的表达式遵循与方法调用相同的规则,该方法调用被视为返回正在构造的类型。
此外,如果存在任何初始值设定项,则安全上下文是所有对象初始值设定项表达式的所有参数和操作数的最小安全上下文。
注意:这些规则的前提是
Span<T>
没有以下形式的构造函数:public Span<T>(ref T p)
这样的构造函数使作为字段使用的
Span<T>
实例和ref
字段无法区分。 本文档中所述的安全规则取决于ref
字段不是 C# 或 .NET 中的有效构造。 尾注