16 个结构
16.1 常规
结构类似于类,它们表示可以包含数据成员和函数成员的数据结构。 但是,与类不同,结构是值类型,不需要堆分配。 类型的变量 struct
直接包含数据的 struct
变量,而类类型的变量包含对数据的引用,后者称为对象。
注意:结构对于具有值语义的小型数据结构特别有用。 复数、坐标系中的点或字典中的键值对都是结构的典型示例。 这些数据结构的关键在于,它们很少有数据成员,它们不需要使用继承或引用语义,而是可以使用赋值复制值而不是引用的值语义方便地实现它们。 end note
如 §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),后跟可选的部分修饰符(§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),可选后跟分号。
结构声明不得提供 type_parameter_constraints_clauses ,除非它还提供 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.2)的含义相同。
修饰 readonly
符指示 struct_declaration 声明实例不可变的类型。
只读结构具有以下约束:
将只读结构实例传递给方法时,其 this
处理方式类似于输入参数/参数,该参数禁止写入任何实例字段(构造函数除外)。
16.2.3 Ref 修饰符
修饰 ref
符指示 struct_declaration 声明在执行堆栈上分配实例的类型。 这些类型称为 ref 结构 类型。 ref
修饰符声明实例可以包含类似 ref 的字段,不应从其安全上下文中复制(§16.4.12)。 用于确定 ref 结构的安全上下文的规则在 §16.4.12 中介绍。
如果在以下任一上下文中使用 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
参数不能在这些上下文中使用。 end note
这些约束可确保类型的 ref struct
变量不引用不再有效的堆栈内存,也不引用不再有效的变量。
16.2.4 部分修饰符
修饰 partial
符指示此 struct_declaration 是分部类型声明。 包含命名空间或类型声明内具有相同名称的多个部分结构声明组合成一个结构声明,遵循 §15.2.7 中指定的规则。
16.2.5 结构接口
结构声明可能包括struct_interfaces规范,在这种情况下,该结构据说直接实现给定接口类型。 对于构造的结构类型,包括泛型类型声明(§15.3.9.7)中声明的嵌套类型,每个实现的接口类型通过替换给定接口中的每个 type_parameter (构造类型的对应 type_argument )获得。
struct_interfaces
: ':' interface_type_list
;
在 §15.2.4.3 中进一步讨论了部分结构声明(§15.2.7)的多个部分接口的处理。
接口实现在 §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)中可用。
注意:除finalizer_declaration外的各种class_member_declaration也是struct_member_declarations。 end note
除了 §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)。
- 不允许结构声明终结器。
16.4.2 值语义
结构是值类型(§8.3),据说具有值语义。 另一方面,类是引用类型(§8.2),据说具有引用语义。
结构类型的变量直接包含结构的数据,而类类型的变量包含对包含数据的对象的引用。 当结构 B
包含类型的 A
实例字段并且 A
是结构类型时,它是一个编译时错误 A
,需要依赖 B
的类型或从 B
中构造的类型。 A struct X
如果包含类型的Y
实例字段,则直接依赖于结构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
相互依赖。end 示例
使用类时,两个变量可以引用同一对象,因此,一个变量上的操作可能会影响另一个变量引用的对象。 使用结构时,变量都有其自己的数据副本(除了按引用参数的情况除外),并且无法对一个参数执行操作来影响另一个参数。 此外,除了显式可为 null(§8.3.12)外,结构类型的值不可能为 null
< a0/>。
注意:如果结构包含引用类型的字段,则其他操作可以更改引用的对象的内容。 但是,字段本身的值(即它引用的对象)不能通过不同结构值的突变来更改。 end note
示例:给定以下内容
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
引用相同的对象。end 示例
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
数组中的每个值初始化为通过将字段y
设置为x
零生成的值。end 示例
结构的默认值对应于结构的默认构造函数返回的值(§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
准备好处理此状态。end note
16.4.6 装箱和取消装箱
类类型的值可以转换为类型 object
,也可以转换为类实现的接口类型,只需在编译时将引用视为另一种类型即可。 同样,可以在不更改引用的情况下将类型 object
或接口类型的值转换为类类型(但在这种情况下,需要运行时类型检查)。
由于结构不是引用类型,因此对于结构类型,这些操作以不同的方式实现。 当结构类型的值转换为某些引用类型(如 §10.2.9 中定义)时,将执行装箱操作。 同样,当某些引用类型的值(如 §10.3.7 中定义)转换为结构类型时,将执行取消装箱操作。 与类类型的相同操作的主要区别在于,装箱和取消装箱 会将结构值复制到 或装箱外实例中。
注意:因此,按照装箱或取消装箱操作,对取消
struct
装箱所做的更改不会反映在装箱struct
中。 end note
有关装箱和取消装箱的更多详细信息,请参阅 §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()
调用时没有发生拳击。end 示例
同样,当在值类型中实现成员时,在约束类型参数上访问成员时,永远不会隐式发生装箱。 例如,假设接口 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
end 示例
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 }
出现错误,因为实例字段声明包括变量初始值设定项。
end 示例
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
withx
和y
initialized 为零。end 示例
不允许结构实例构造函数包含表单 base(
argument_list)
的构造函数初始值设定项,其中 argument_list 是可选的。
this
结构实例构造函数的参数对应于结构类型的输出参数。 因此, this
应在构造函数返回的每个位置明确分配 (§9.4)。 同样,在明确分配构造函数正文之前,它不能在构造函数正文中读取(甚至隐式)。
如果结构实例构造函数指定构造函数初始值设定项,则初始值设定项被视为在构造函数正文之前发生的此明确赋值。 因此,正文本身没有初始化要求。
示例:请考虑以下实例构造函数实现:
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 } }
end example]
16.4.10 静态构造函数
结构的静态构造函数遵循与类相同的大多数规则。 结构类型的静态构造函数的执行由应用程序域中发生的以下事件中的第一个触发:
- 引用结构类型的静态成员。
- 调用结构类型的显式声明构造函数。
注意:创建结构类型的默认值(§16.4.5)不会触发静态构造函数。 (例如,这是数组中元素的初始值。 end note
16.4.11 自动实现的属性
自动实现的属性(§15.7.4)使用隐藏的后盾字段,这些字段只能访问属性访问器。
注意:此访问限制意味着包含自动实现属性的结构中的构造函数通常需要显式构造函数初始值设定项,在这种情况下,它们不需要显式构造函数初始值设定项,以满足在调用任何函数成员或构造函数返回之前明确分配的所有字段的要求。 end note
16.4.12 安全上下文约束
16.4.12.1 常规
在编译时,每个表达式都与一个上下文相关联,在该上下文中可以安全地访问该实例及其所有字段,以及其安全上下文。 安全上下文是一个上下文,包含表达式,该值可以安全地转义到该表达式。
编译时类型不是 ref 结构的任何表达式都具有调用方上下文的安全上下文。
default
对于任何类型的表达式,具有调用方上下文的安全上下文。
对于编译时类型为 ref 结构的任何非默认表达式,其安全上下文由以下部分定义。
安全上下文记录可将值复制到哪个上下文中。 给定从具有安全上下文的表达式E1
到具有安全上下文S1
S2
的表达式E2
的赋值,如果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.12.2 参数安全上下文
ref 结构类型的参数(包括 this
实例方法的参数)具有调用方上下文的安全上下文。
16.4.12.3 局部变量安全上下文
ref 结构类型的局部变量具有安全上下文,如下所示:
- 如果变量是循环的
foreach
迭代变量,则变量的安全上下文与循环表达式的安全上下文foreach
相同。 - 否则,如果变量的声明具有初始值设定项,则变量的安全上下文与该初始值设定项的安全上下文相同。
- 否则,变量在声明点未初始化,并且具有调用方上下文的安全上下文。
16.4.12.4 字段安全上下文
对字段 e.F
的 F
引用,其中类型为 ref 结构类型,具有与安全上下文相同的安全上下文 e
。
16.4.12.5 运算符
用户定义的运算符的应用程序被视为方法调用(§16.4.12.6)。
对于生成值(例如 e1 + e2
或 c ? e1 : e2
)的运算符,结果的安全上下文是运算符操作数的安全上下文中最窄的上下文。 因此,对于生成值的一元运算符,例如 +e
,结果的安全上下文是操作数的安全上下文。
注意:条件运算符的第一个操作数是一个
bool
,因此其安全上下文为调用方上下文。 随后,生成的安全上下文是第二和第三个操作数最窄的安全上下文。 end note
16.4.12.6 方法和属性调用
由方法调用 e1.M(e2, ...)
或属性调用 e.P
生成的值具有以下上下文中最小的安全上下文:
- 调用方上下文。
- 所有参数表达式(包括接收方)的安全上下文。
属性调用(或get
set
)被视为上述规则对基础方法的方法调用。
16.4.12.7 stackalloc
stackalloc 表达式的结果具有函数成员的安全上下文。
16.4.12.8 构造函数调用
new
调用构造函数的表达式遵循与方法调用相同的规则,该方法调用被视为返回正在构造的类型。
此外,如果存在任何初始值设定项,则安全上下文是所有对象初始值设定项表达式的所有参数和操作数的最小安全上下文。
注意:这些规则依赖于
Span<T>
以下形式的构造函数:public Span<T>(ref T p)
此类构造函数使用作字段的
Span<T>
实例与字段不可ref
区分。 本文档中所述的安全规则取决于ref
字段不是 C# 或 .NET 中的有效构造。 end note