9.1 常规
变量表示存储位置。 每个变量都有一个类型,该类型确定可以在变量中存储哪些值。 C# 是类型安全的语言,C# 编译器保证存储在变量中的值始终是适当的类型。 变量的值可以通过赋值或使用 ++
和 --
运算符来更改。
在获取变量的值之前,该变量必须被明确赋值 (§9.4)。
如下述子子句所述,变量要么是初始已赋值的,要么是初始未赋值的。 初始已赋值的变量具有定义良好的初始值,并且始终被视为明确赋值的。 初始未赋值的变量没有初始值。 要使初始未赋值的变量在某个位置被视为明确赋值的,在通向该位置的每个可能的执行路径中都必须对该变量进行赋值。
9.2 变量类别
9.2.1 常规
C# 定义了八类变量:静态变量、实例变量、数组元素、值参数、输入参数、引用参数、输出参数和局部变量。 后面的子子句将描述每一类变量。
示例:在以下代码中
class A { public static int x; int y; void F(int[] v, int a, ref int b, out int c, in int d) { int i = 1; c = a + b++ + d; } }
x
是静态变量,y
是实例变量,v[0]
是数组元素,a
是值参数,b
是引用参数,c
是输出参数,d
是输入参数,i
是局部变量。 结束示例
9.2.2 静态变量
用 static
修饰符声明的字段是静态变量。 静态变量在其包含类型的 static
构造函数 (§15.12) 执行之前存在,并在关联的应用程序域不存在时停止存在。
静态变量的初始值是该变量类型的默认值 (§9.3)。
出于明确赋值检查的目的,静态变量被视为初始已赋值的。
9.2.3 实例变量
9.2.3.1 常规
未用 static
修饰符声明的字段是实例变量。
9.2.3.2 类中的实例变量
类的实例变量在创建该类的新实例时存在,并在没有对该实例的引用且该实例的终结器(如果有)已执行时停止存在。
类的实例变量的初始值是该变量类型的默认值 (§9.3)。
出于明确赋值检查的目的,类的实例变量被视为初始已赋值的。
9.2.3.3 结构中的实例变量
结构的实例变量与它所属的结构变量具有完全相同的生存期。 换句话说,当结构类型的变量存在或不存在时,该结构的实例变量也会相应地存在或不存在。
结构的实例变量的初始赋值状态与包含的 struct
变量的初始赋值状态相同。 换句话说,当结构变量被视为初始已赋值的时,其实例变量也被视为初始已赋值的;当结构变量被视为初始未赋值的时,其实例变量同样被视为未赋值的。
9.2.4 数组元素
数组的元素在创建数组实例时存在,并在没有对该数组实例的引用时停止存在。
数组的每个元素的初始值是数组元素类型的默认值 (§9.3)。
出于明确赋值检查的目的,数组元素被视为初始已赋值的。
9.2.5. 值参数
值参数在调用其所属的函数成员(方法、实例构造函数、访问器或运算符)或匿名函数时存在,并使用调用中给定的参数值进行初始化。 值参数通常在函数体执行完成时停止存在。 但是,如果值参数被匿名函数捕获 (§12.19.6.2),其生存期至少延续到从该匿名函数创建的委托或表达式树符合垃圾回收条件为止。
出于明确赋值检查的目的,值参数被视为初始已赋值的。
值参数将在 §15.6.2.2 中进一步讨论。
9.2.6 引用参数
引用参数是引用变量 (§9.7),在调用函数成员、委托、匿名函数或局部函数时存在,其引用对象被初始化为该调用中作为参数给出的变量。 引用参数在函数体执行完成时停止存在。 与值参数不同,引用参数不得被捕获 (§9.7.2.9)。
以下明确赋值规则适用于引用参数。
注意:输出参数的规则不同,将在 (§9.2.7) 中描述。 尾注
- 在函数成员或委托调用中,变量在作为引用参数传递之前必须被明确赋值 (§9.4)。
- 在函数成员或匿名函数中,引用参数被视为初始已赋值的。
引用参数将在 §15.6.2.3.3 中进一步讨论。
9.2.7 输出参数
输出参数是引用变量 (§9.7),在调用函数成员、委托、匿名函数或局部函数时存在,其引用对象被初始化为该调用中作为参数给出的变量。 输出参数在函数体执行完成时停止存在。 与值参数不同,输出参数不得被捕获 (§9.7.2.9)。
以下明确赋值规则适用于输出参数。
注意:引用参数的规则不同,将在 (§9.2.6) 中描述。 尾注
- 变量在作为输出参数传递给函数成员或委托调用之前无需被明确赋值。
- 函数成员或委托调用正常完成后,在该执行路径中,每个作为输出参数传递的变量都被视为已赋值。
- 在函数成员或匿名函数中,输出参数被视为初始未赋值的。
- 函数成员、匿名函数或局部函数的每个输出参数在该函数成员、匿名函数或局部函数正常返回之前必须被明确赋值 (§9.4)。
输出参数将在 §15.6.2.3.4 中进一步讨论。
9.2.8 输入参数
输入参数是引用变量 (§9.7),在调用函数成员、委托、匿名函数或局部函数时存在,其引用对象被初始化为该调用中作为参数给出的 variable_reference。 输入参数在函数体执行完成时停止存在。 与值参数不同,输入参数不应被捕获 (§9.7.2.9)。
以下明确赋值规则适用于输入参数。
- 变量在作为函数成员或委托调用中的输入参数传递之前,必须经过明确赋值 (§9.4)。
- 在函数成员、匿名函数或局部函数中,输入参数被视为初始已赋值。
输入参数在 §15.6.2.3.2 中进一步讨论。
9.2.9 局部变量
9.2.9.1 常规
局部变量通过 try_statement 的 local_variable_declaration、declaration_expression、foreach_statement 或 specific_catch_clause 声明。 局部变量也可通过某些类型的 pattern (§11) 声明。 对于 foreach_statement,局部变量是迭代变量 (§13.9.5)。 对于 specific_catch_clause,局部变量是异常变量 (§13.11)。 通过 foreach_statement 或 specific_catch_clause 声明的局部变量被视为初始已赋值。
local_variable_declaration 可出现在 block、for_statement、switch_block 或 using_statement 中。
declaration_expression 可作为 out
argument_value 出现,也可作为作为解构赋值目标的 tuple_element 出现 (§12.21.2)。
局部变量的生存期是程序执行期间保证为其保留存储的部分。 此生存期从进入与其关联的范围开始,至少持续到该范围以某种方式结束。 (进入封闭块、调用方法或从迭代器块生成值会暂停但不会结束当前范围的执行。)如果局部变量被匿名函数捕获 (§12.19.6.2),其生存期至少延长到从匿名函数创建的委托或表达式树以及任何其他引用所捕获变量的对象符合垃圾回收条件为止。 如果父范围以递归或迭代方式进入,每次都会创建局部变量的新实例,并且其初始值设定项(如有)每次都会被计算。
注意:每次进入局部变量的范围时,都会实例化该局部变量。 这种行为对包含匿名方法的用户代码可见。 尾注
注意::由 foreach_statement 声明的迭代变量 (§13.9.5) 的生存期是该语句的单次迭代。 每次迭代都会创建一个新变量。 尾注
注意:局部变量的实际生存期取决于实现。 例如,编译器可能静态确定块中的局部变量仅在该块的一小部分中使用。 使用此分析,编译器可以生成代码,导致变量的存储生存期比包含块短。
局部引用变量所引用的存储独立于该局部引用变量的生存期回收 (§7.9)。
尾注
由 local_variable_declaration 或 declaration_expression 引入的局部变量不会自动初始化,因此没有默认值。 此类局部变量被视为初始未赋值。
注意:包含初始值设定项的 local_variable_declaration 仍然是初始未赋值的。 声明的执行行为与对变量的赋值完全相同 (§9.4.4.5)。 在初始值设定项执行之前使用变量;例如,在初始值设定项表达式本身内或通过使用绕过初始值设定项的 goto_statement;是编译时错误:
goto L; int x = 1; // never executed L: x += 1; // error: x not definitely assigned
在局部变量的范围内,在其声明符之前的文本位置引用该局部变量是编译时错误。
尾注
9.2.9.2 弃元
弃元是没有名称的局部变量。 弃元通过带有标识符 的声明表达式 (_
) 引入;并且是隐式类型(_
或 var _
)或显式类型 (T _
)。
注意:
_
在许多形式的声明中是有效的标识符。 尾注
由于弃元没有名称,对其所代表的变量的唯一引用是引入它的表达式。
注意:但是,弃元可以作为输出参数传递,允许相应的输出参数表示其关联的存储位置。 尾注
弃元初始未赋值,因此访问其值始终是错误。
示例:
_ = "Hello".Length; (int, int, int) M(out int i1, out int i2, out int i3) { ... } (int _, var _, _) = M(out int _, out var _, out _);
该示例假定范围内没有名称
_
的声明。对
_
的赋值显示了忽略表达式结果的简单模式。M
的调用显示了元组中可用的不同形式的弃元以及作为输出参数的弃元。结束示例
9.3 默认值
以下类别的变量会自动初始化为其默认值:
- 静态变量。
- 类实例的实例变量。
- 数组元素。
变量的默认值取决于变量的类型,并确定如下:
- 对于 value_type 的变量,默认值与由 value_type 的默认构造函数计算的值相同 (§8.3.3)。
- 对于 reference_type 的变量,默认值为
null
。
注意:默认值初始化通常通过让内存管理器或垃圾回收器在分配内存供使用之前将内存初始化为全零位来完成。 因此,使用全零位表示空引用很方便。 尾注
9.4 明确赋值
9.4.1 常规
在函数成员或匿名函数的可执行代码中的给定位置,如果编译器可以通过特定的静态流分析(§9.4.4)证明变量已自动初始化或已作为至少一个赋值的目标,则变量被认为是 确定已赋值。
注意:通俗地说,明确赋值规则如下:
- 初始已赋值变量 (§9.4.2) 始终被视为明确赋值。
- 初始未赋值变量 (§9.4.3) 在给定位置被视为明确赋值,如果通向该位置的所有可能执行路径都包含以下至少一项:
- 变量作为左操作数的简单赋值 (§12.21.2)。
- 调用表达式(§12.8.10) 或对象创建表达式(§12.8.17.2),将变量作为输出参数传递。
- 对于局部变量,包含变量初始值设定项的变量的局部变量声明 (§13.6.2)。
上述通俗规则的基础正式规范在 §9.4.2、§9.4.3 和 §9.4.4 中描述。
尾注
struct_type 变量的实例变量的明确赋值状态既单独跟踪也集体跟踪。 除了 §9.4.2、§9.4.3 和 §9.4.4 中描述的规则外,以下规则适用于 struct_type 变量及其实例变量:
- 如果包含它的 struct_type 变量被视为明确赋值,则实例变量被视为明确赋值。
- 如果 struct_type 变量的每个实例变量都被视为明确赋值,则该结构类型变量被视为明确赋值。
在以下上下文中需要明确赋值:
变量在获取其值的每个位置都必须明确赋值。
注意:这确保永远不会出现未定义的值。 尾注
变量在表达式中的出现被视为获取变量的值,除非
- 变量是简单赋值的左操作数,
- 变量作为输出参数传递,或者
- 变量是 struct_type 变量并作为成员访问的左操作数出现。
变量在作为引用参数传递的每个位置都必须明确赋值。
注意:这确保被调用的函数成员可以认为引用参数初始已赋值。 尾注
变量在作为输入参数传递的每个位置都必须明确赋值。
注意:这确保被调用的函数成员可以认为输入参数初始已赋值。 尾注
函数成员的所有输出参数在函数成员返回的每个位置(通过 return 语句或执行到达函数成员主体的末尾)都必须明确赋值。
注释:这可确保函数成员不会在输出参数中返回未定义的值,从而使编译器能够考虑将变量作为等效于变量赋值的输出参数的函数成员调用。 尾注
this
实例构造函数的 变量在该实例构造函数返回的每个位置都必须明确赋值。
9.4.2 初始已赋值变量
以下类别的变量被归类为初始已赋值:
- 静态变量。
- 类实例的实例变量。
- 初始已赋值结构变量的实例变量。
- 数组元素。
- 值参数。
- 引用参数。
- 输入参数。
- 在
catch
子句或foreach
语句中声明的变量。
9.4.3 初始未赋值变量
以下类别的变量被归类为初始未赋值:
- 初始未赋值结构变量的实例变量。
- 输出参数,包括没有构造函数初始值设定项的结构实例构造函数的
this
变量。 - 局部变量,但在
catch
子句或foreach
语句中声明的变量除外。
9.4.4 确定明确赋值的精确规则
9.4.4.1 常规
为了确定明确分配每个已用变量,编译器应使用与此子引用中所述的变量等效的进程。
函数成员的正文可以声明一个或多个最初未分配的变量。 对于每个初始未赋值的变量 v,编译器应在函数成员中的以下每个点为 v 确定明确赋值状态:
- 在每个语句的开头
- 在每个语句的终点 (§13.2)
- 在将控制转移到另一个语句或语句终点的每个弧上
- 在每个表达式的开头
- 在每个表达式的结尾
v 的明确赋值状态可以是:
- 明确赋值。 这表明在所有可能的控制流到此点时,v 已被赋值。
- 未明确赋值。 对于
bool
类型的表达式末尾的变量状态,未明确赋值的变量状态可能(但不一定)属于以下子状态之一:- 表达式为 true 后明确赋值。 此状态表明如果布尔表达式计算为 true,则 v 明确赋值,但如果布尔表达式计算为 false,则不一定赋值。
- 表达式为 false 后明确赋值。 此状态表明如果布尔表达式计算为 false,则 v 明确赋值,但如果布尔表达式计算为 true,则不一定赋值。
以下规则控制变量 v 的状态在每个位置的确定方式。
9.4.4.2 语句的一般规则
- 在函数成员主体的开头,v 未明确赋值。
- 任何其他语句开头的 v 的明确赋值状态通过检查所有以该语句开头为目标的控制流转移上的 v 的明确赋值状态来确定。 当且仅当在所有此类控制流转移上 v 都明确赋值时,v 在语句开头才明确赋值。 可能的控制流转移集的确定方式与检查语句可访问性 (§13.2) 的方式相同。
-
、
block
、checked
、unchecked
、if
、while
、do
、for
、foreach
、lock
或using
语句终点的switch
的明确赋值状态通过检查所有以该语句终点为目标的控制流转移上的 v 的明确赋值状态来确定。 如果在所有此类控制流转移上 v 都明确赋值,则 v 在语句的终点明确赋值。 否则,v 在语句的终点未明确赋值。 可能的控制流转移集的确定方式与检查语句可访问性 (§13.2) 的方式相同。
注意:由于没有到不可访问语句的控制路径,因此在任何不可访问语句的开头,v 都明确赋值。 尾注
9.4.4.3 块语句、checked 和 unchecked 语句
到块中语句列表的第一个语句(或到块的终点,如果语句列表为空)的控制转移上的 v 的明确赋值状态与块、 或 checked
语句之前的 unchecked
的明确赋值状态相同。
9.4.4.4 表达式语句
对于由表达式 expr 组成的表达式语句 stmt:
- v 在 expr 开头的明确赋值状态与在 stmt 开头的相同。
- 如果 v 在 expr 末尾明确赋值,则它在 stmt 的终点明确赋值;否则,它在 stmt 的终点未明确赋值。
9.4.4.5 声明语句
- 如果 stmt 是没有初始值设定项的声明语句,则 v 在 stmt 终点的明确赋值状态与在 stmt 开头的相同。
- 如果 stmt 是带有初始值设定项的声明语句,则 v 的明确赋值状态的确定方式就好像 stmt 是语句列表,每个带有初始值设定项的声明都有一个赋值语句(按声明顺序)。
9.4.4.6 If 语句
对于以下形式的语句 stmt:
if ( «expr» ) «then_stmt» else «else_stmt»
- v 在 expr 开头的明确赋值状态与在 stmt 开头的相同。
- 如果 v 在 expr 末尾处于“表达式为 true 后明确赋值”状态,则它在到 then_stmt 的控制流转移上明确赋值,而在到 else_stmt 或到 stmt 终点(如果没有 else 子句)的控制流转移上未明确赋值。
- 如果 v 在 expr 末尾处于“表达式为 false 后明确赋值”状态,则它在到 then_stmt 的控制流转移上明确赋值,而在到 else_stmt 或 stmt 终点的控制流转移上未明确赋值。
- 如果 v 在 expr 末尾处于“表达式为 false 后明确赋值”状态,则它在到 else_stmt 的控制流转移上明确赋值,而在到 then_stmt 的控制流转移上未明确赋值。 当且仅当它在 then_stmt 的终点明确赋值时,它在 stmt 的终点明确赋值。
- 否则,v 在到 then_stmt 或 else_stmt 的控制流转移上,或在到 stmt 终点(如果没有 else 子句)的控制流转移上被视为未明确赋值。
9.4.4.7 Switch 语句
对于带有控制表达式 switch
的 语句 stmt:
v 在 expr 开头的明确赋值状态与在 stmt 开头的 v 的状态相同。
case 的保护子句开头的 v 的明确赋值状态是
- 如果 v 是在 switch_label 中声明的模式变量:“明确赋值”。
- 如果包含该保护子句的 switch 标签 (§13.8.3) 不可访问:“明确赋值”。
- 否则,v 的状态与 expr 之后的 v 的状态相同。
示例:第二条规则消除了在不可达代码中访问未分配变量时编译器发出错误的必要。 在不可访问的 switch 标签 中,
case 2 when b
的状态是“明确赋值”。bool b; switch (1) { case 2 when b: // b is definitely assigned here. break; }
结束示例
到可访问的 switch 块语句列表的控制流转移上的 v 的明确赋值状态是
- 如果控制转移是由于“goto case”或“goto default”语句引起的,则 v 的状态与该“goto”语句开头的状态相同。
- 如果控制转移是由于 switch 的
default
标签引起的,则 v 的状态与 expr 之后的 v 的状态相同。 - 如果控制转移是由于不可访问的 switch 标签引起的,则 v 的状态是“明确赋值”。
- 如果控制转移是由于带有保护子句的可访问 switch 标签引起的,则 v 的状态与保护子句之后的 v 的状态相同。
- 如果控制转移是由于没有保护子句的可访问 switch 标签引起的,则 v 的状态是
- 如果 v 是在 switch_label 中声明的模式变量:“明确赋值”。
- 否则,v 的状态与 expr 之后的 v 的状态相同。
这些规则的一个结果是,如果在其部分中不是唯一可访问的 switch 标签,则在 switch_label 中声明的模式变量在其 switch 部分的语句中将“未明确赋值”。
示例:
public static double ComputeArea(object shape) { switch (shape) { case Square s when s.Side == 0: case Circle c when c.Radius == 0: case Triangle t when t.Base == 0 || t.Height == 0: case Rectangle r when r.Length == 0 || r.Height == 0: // none of s, c, t, or r is definitely assigned return 0; case Square s: // s is definitely assigned return s.Side * s.Side; case Circle c: // c is definitely assigned return c.Radius * c.Radius * Math.PI; … } }
结束示例
9.4.4.8 While 语句
对于以下形式的语句 stmt:
while ( «expr» ) «while_body»
- v 在 expr 开头的明确赋值状态与在 stmt 开头的相同。
- 如果 v 在 expr 末尾明确赋值,则它在到 while_body 和到 stmt 终点的控制流转移上明确赋值。
- 如果 v 在 expr 末尾处于“表达式为 true 后明确赋值”状态,则它在到 while_body 的控制流转移上明确赋值,但在 stmt 的终点未明确赋值。
- 如果 v 在 expr 末尾处于“表达式为 false 后明确赋值”状态,则它在到 stmt 终点的控制流转移上明确赋值,但在到 while_body 的控制流转移上未明确赋值。
9.4.4.9 Do 语句
对于以下形式的语句 stmt:
do «do_body» while ( «expr» ) ;
- v 在从 stmt 开头到 do_body 的控制流转移上的明确赋值状态与在 stmt 开头的相同。
- v 在 expr 开头的明确赋值状态与在 do_body 终点的相同。
- 如果 v 在 expr 末尾明确赋值,则它在到 stmt 终点的控制流转移上明确赋值。
- 如果 v 在 expr 末尾处于“表达式为 false 后明确赋值”状态,则它在到 stmt 终点的控制流转移上明确赋值,但在到 do_body 的控制流转移上未明确赋值。
9.4.4.10 For 语句
对于以下形式的语句:
for ( «for_initializer» ; «for_condition» ; «for_iterator» )
«embedded_statement»
明确赋值检查的执行方式就好像该语句编写为:
{
«for_initializer» ;
while ( «for_condition» )
{
«embedded_statement» ;
LLoop: «for_iterator» ;
}
}
其中以 continue
语句为目标的 for
语句被转换为以标签 goto
为目标的 LLoop
语句。 如果从 语句中省略 for
,则明确赋值的计算将按上述扩展中 for_condition 被替换为 true 的方式进行。
9.4.4.11 Break、continue 和 goto 语句
由 、break
或 continue
语句引起的控制流转移上的 goto
的明确赋值状态与该语句开头的 v 的明确赋值状态相同。
9.4.4.12 Throw 语句
对于以下形式的语句 stmt:
throw «expr» ;
v 在 expr 开头的明确赋值状态与在 stmt 开头的 v 的明确赋值状态相同。
9.4.4.13 Return 语句
对于以下形式的语句 stmt:
return «expr» ;
- v 在 expr 开头的明确赋值状态与在 stmt 开头的 v 的明确赋值状态相同。
- 如果 v 是输出参数,则它必须在以下任一位置明确赋值:
- expr 之后
- 或者在包围
finally
语句的try
-finally
或try
-catch
-finally
的return
块的末尾。
对于以下形式的语句 stmt:
return ;
- 如果 v 是输出参数,则它必须在以下任一位置明确赋值:
- stmt 之前
- 或者在包围
finally
语句的try
-finally
或try
-catch
-finally
的return
块的末尾。
9.4.4.14 Try-catch 语句
对于以下形式的语句 stmt:
try «try_block»
catch ( ... ) «catch_block_1»
...
catch ( ... ) «catch_block_n»
- v 在 try_block 开头的明确赋值状态与在 stmt 开头的 v 的明确赋值状态相同。
- v 在 catch_block_i 开头(对于任何 i)的明确赋值状态与在 stmt 开头的 v 的明确赋值状态相同。
- v 在 stmt 终点的明确赋值状态明确赋值,当且仅当 v 在 try_block 的终点和每个 catch_block_i(对于从 1 到 n 的每个 i)的终点明确赋值。
9.4.4.15 Try-finally 语句
对于以下形式的语句 stmt:
try «try_block» finally «finally_block»
- v 在 try_block 开头的明确赋值状态与在 stmt 开头的 v 的明确赋值状态相同。
- v 在 finally_block 开头的明确赋值状态与在 stmt 开头的 v 的明确赋值状态相同。
-
v 在 stmt 终点的明确赋值状态明确赋值,当且仅当以下至少一项为真:
- v 在 try_block 的终点明确赋值
- v 在 finally_block 的终点明确赋值
如果进行了从 goto
内部开始并在 try_block 外部结束的控制流转移(例如 语句),则如果 v 在 finally_block 的终点明确赋值,v 在该控制流转移上也被视为明确赋值。 (这不是唯一条件 — 如果由于其他原因 v 在该控制流转移上明确赋值,则它仍然被视为明确赋值。)
9.4.4.16 Try-catch-finally 语句
对于以下形式的语句:
try «try_block»
catch ( ... ) «catch_block_1»
...
catch ( ... ) «catch_block_n»
finally «finally_block»
明确赋值分析的执行方式就好像该语句是包围 try
-finally
语句的 try
-catch
语句:
try
{
try «try_block»
catch ( ... ) «catch_block_1»
...
catch ( ... ) «catch_block_n»
}
finally «finally_block»
示例:以下示例演示
try
语句 (§13.11) 的不同块如何影响明确赋值。class A { static void F() { int i, j; try { goto LABEL; // neither i nor j definitely assigned i = 1; // i definitely assigned } catch { // neither i nor j definitely assigned i = 3; // i definitely assigned } finally { // neither i nor j definitely assigned j = 5; // j definitely assigned } // i and j definitely assigned LABEL: ; // j definitely assigned } }
结束示例
9.4.4.17 Foreach 语句
对于以下形式的语句 stmt:
foreach ( «type» «identifier» in «expr» ) «embedded_statement»
- v 在 expr 开头的明确赋值状态与在 stmt 开头的 v 的状态相同。
- v 在到 embedded_statement 或到 stmt 终点的控制流转移上的明确赋值状态与在 expr 末尾的 v 的状态相同。
9.4.4.18 Using 语句
对于以下形式的语句 stmt:
using ( «resource_acquisition» ) «embedded_statement»
- v 在 resource_acquisition 开头的明确赋值状态与在 stmt 开头的 v 的状态相同。
- v 在到 embedded_statement 的控制流转移上的明确赋值状态与在 resource_acquisition 末尾的 v 的状态相同。
9.4.4.19 Lock 语句
对于以下形式的语句 stmt:
lock ( «expr» ) «embedded_statement»
- v 在 expr 开头的明确赋值状态与在 stmt 开头的 v 的状态相同。
- v 在到 embedded_statement 的控制流转移上的明确赋值状态与在 expr 末尾的 v 的状态相同。
9.4.4.20 Yield 语句
对于以下形式的语句 stmt:
yield return «expr» ;
- v 在 expr 开头的明确赋值状态与在 stmt 开头的 v 的状态相同。
- v 在 stmt 末尾的明确赋值状态与在 expr 末尾的 v 的状态相同。
yield break
语句对明确赋值状态没有影响。
9.4.4.21 常量表达式的通用规则
以下内容适用于任何常量表达式,并优先于可能适用的以下子项中的任何规则:
对于值为 true
的常量表达式:
- 如果在表达式之前 v 已明确赋值,则在表达式之后 v 已明确赋值。
- 否则,在表达式之后 v 为“在 false 表达式之后明确赋值”。
示例:
int x; if (true) {} else { Console.WriteLine(x); }
结束示例
对于值为 false
的常量表达式:
- 如果在表达式之前 v 已明确赋值,则在表达式之后 v 已明确赋值。
- 否则,在表达式之后 v 为“在 true 表达式之后明确赋值”。
示例:
int x; if (false) { Console.WriteLine(x); }
结束示例
对于所有其他常量表达式,表达式之后 v 的明确赋值状态与表达式之前 v 的明确赋值状态相同。
9.4.4.22 简单表达式的通用规则
以下规则适用于这些类型的表达式:文本(
- 此类表达式结束时 v 的明确赋值状态与表达式开始时 v 的明确赋值状态相同。
9.4.4.23 带有嵌入表达式的表达式的通用规则
以下规则适用于这些类型的表达式:括号表达式 (§12.8.5)、元组表达式 (§12.8.6)、元素访问表达式 (§12.8.12)、带索引的基本访问表达式 (§12.8.15)、增量和减量表达式(§12.8.16、§12.9.6)、强制转换表达式 (§12.9.7)、一元 +
、-
、~
、*
表达式、二进制 +
、-
、*
、/
、%
、<<
、>>
、<
、<=
、>
、>=
、==
、!=
、is
、as
、&
、|
、^
表达式(§12.10、§12.11、§12.12、§12.13)、复合赋值表达式 (§12.21.4)、checked
和 unchecked
表达式 (§12.8.20)、数组和委托创建表达式 (§12.8.17) 和 await
表达式 (§12.9.8)。
这些表达式中的每一个都有一个或多个子表达式,这些子表达式按固定顺序无条件计算。
示例:二元
%
运算符先计算运算符的左侧,然后计算右侧。 索引操作先计算被索引的表达式,然后按从左到右的顺序计算每个索引表达式。 结束示例
对于表达式 expr,其具有按顺序计算的子表达式 expr₁、expr₂、…、exprₓ:
- expr₁ 开始时 v 的明确赋值状态与 expr 开始时的明确赋值状态相同。
- exprᵢ(i 大于 1)开始时 v 的明确赋值状态与 exprᵢ₋₁ 结束时的明确赋值状态相同。
- expr 结束时 v 的明确赋值状态与 exprₓ 结束时的明确赋值状态相同。
9.4.4.24 调用表达式和对象创建表达式
如果要调用的方法是没有实现分部方法声明的分部方法,或者是省略了调用的条件方法(§22.5.3.2),则调用之后 v 的明确赋值状态与调用之前 v 的明确赋值状态相同。 否则,适用以下规则:
对于形式为以下的调用表达式 expr:
«primary_expression» ( «arg₁», «arg₂», … , «argₓ» )
或形式为以下的对象创建表达式 expr:
new «type» ( «arg₁», «arg₂», … , «argₓ» )
- 对于调用表达式,primary_expression 之前 v 的明确赋值状态与 expr 之前 v 的状态相同。
- 对于调用表达式,arg₁ 之前 v 的明确赋值状态与 primary_expression 之后 v 的状态相同。
- 对于对象创建表达式,arg₁ 之前 v 的明确赋值状态与 expr 之前 v 的状态相同。
- 对于每个参数 argᵢ,argᵢ 之后 v 的明确赋值状态由正常表达式规则确定,忽略任何
in
、out
或ref
修饰符。 - 对于任何 i 大于 1 的参数 argᵢ,argᵢ 之前 v 的明确赋值状态与 argᵢ₋₁ 之后 v 的状态相同。
- 如果变量 v 作为任何参数中的
out
参数(即形式为“out v”的参数)传递,则 expr 之后 v 的状态为已明确赋值。 否则,expr 之后 v 的状态与 argₓ 之后 v 的状态相同。 - 对于数组初始值设定项(§12.8.17.4)、对象初始值设定项(§12.8.17.2.2)、集合初始值设定项(§12.8.17.2.3)和匿名对象初始值设定项(§12.8.17.3),明确赋值状态由定义这些构造的扩展确定。
9.4.4.25 简单赋值表达式
设表达式 e 中的赋值目标集定义如下:
- 如果 e 是元组表达式,则 e 中的赋值目标是 e 的元素的赋值目标的并集。
- 否则,e 中的赋值目标是 e。
对于以下形式的表达式 expr:
«expr_lhs» = «expr_rhs»
- expr_lhs 之前 v 的明确赋值状态与 expr 之前 v 的明确赋值状态相同。
- expr_rhs 之前 v 的明确赋值状态与 expr_lhs 之后 v 的明确赋值状态相同。
- 如果 v 是 expr_lhs 的赋值目标,则 expr 之后 v 的明确赋值状态为已明确赋值。 否则,如果赋值发生在结构类型的实例构造函数中,且 v 是正在构造的实例上自动实现的属性 P 的隐藏支持字段,且指定 P 的属性访问是 expr_lhs 的赋值目标,则 expr 之后 v 的明确赋值状态为已明确赋值。 否则,expr 之后 v 的明确赋值状态与 expr_rhs 之后 v 的明确赋值状态相同。
示例:在以下代码中
class A { static void F(int[] arr) { int x; arr[x = 1] = x; // ok } }
变量
x
在arr[x = 1]
作为第二个简单赋值的左侧计算后被视为已明确赋值。结束示例
9.4.4.26 && 表达式
对于以下形式的表达式 expr:
«expr_first» && «expr_second»
- expr_first 之前 v 的明确赋值状态与 expr 之前 v 的明确赋值状态相同。
- expr_second 之前 v 的明确赋值状态为已明确赋值,当且仅当 expr_first 之后 v 的状态为已明确赋值或“在 true 表达式之后明确赋值”。 否则,它未明确赋值。
-
expr 之后 v 的明确赋值状态由以下确定:
- 如果 expr_first 之后 v 的状态为已明确赋值,则 expr 之后 v 的状态为已明确赋值。
- 否则,如果 expr_second 之后 v 的状态为已明确赋值,且 expr_first 之后 v 的状态为“在 false 表达式之后明确赋值”,则 expr 之后 v 的状态为已明确赋值。
- 否则,如果 expr_second 之后 v 的状态为已明确赋值或“在 true 表达式之后明确赋值”,则 expr 之后 v 的状态为“在 true 表达式之后明确赋值”。
- 否则,如果 expr_first 之后 v 的状态为“在 false 表达式之后明确赋值”,且 expr_second 之后 v 的状态为“在 false 表达式之后明确赋值”,则 expr 之后 v 的状态为“在 false 表达式之后明确赋值”。
- 否则,expr 之后 v 的状态未明确赋值。
示例:在以下代码中
class A { static void F(int x, int y) { int i; if (x >= 0 && (i = y) >= 0) { // i definitely assigned } else { // i not definitely assigned } // i not definitely assigned } }
变量
i
在if
语句的一个嵌入语句中被视为已明确赋值,但在另一个中不被视为已明确赋值。 在方法if
的F
语句中,变量i
在第一个嵌入语句中已明确赋值,因为表达式(i = y)
的执行总是在该嵌入语句的执行之前。 相反,变量i
在第二个嵌入语句中未明确赋值,因为x >= 0
可能已测试为 false,导致变量i
未赋值。结束示例
9.4.4.27 || 表达式
对于以下形式的表达式 expr:
«expr_first» || «expr_second»
- expr_first 之前 v 的明确赋值状态与 expr 之前 v 的明确赋值状态相同。
- expr_second 之前 v 的明确赋值状态为已明确赋值,当且仅当 expr_first 之后 v 的状态为已明确赋值或“在 true 表达式之后明确赋值”。 否则,它未明确赋值。
-
expr 之后 v 的明确赋值语句由以下确定:
- 如果 expr_first 之后 v 的状态为已明确赋值,则 expr 之后 v 的状态为已明确赋值。
- 否则,如果 expr_second 之后 v 的状态为已明确赋值,且 expr_first 之后 v 的状态为“在 true 表达式之后明确赋值”,则 expr 之后 v 的状态为已明确赋值。
- 否则,如果 expr_second 之后 v 的状态为已明确赋值或“在 false 表达式之后明确赋值”,则 expr 之后 v 的状态为“在 false 表达式之后明确赋值”。
- 否则,如果 expr_first 之后 v 的状态为“在 true 表达式之后明确赋值”,且 expr_second 之后 v 的状态为“在 true 表达式之后明确赋值”,则 expr 之后 v 的状态为“在 true 表达式之后明确赋值”。
- 否则,expr 之后 v 的状态未明确赋值。
示例:在以下代码中
class A { static void G(int x, int y) { int i; if (x >= 0 || (i = y) >= 0) { // i not definitely assigned } else { // i definitely assigned } // i not definitely assigned } }
变量
i
在if
语句的一个嵌入语句中被视为已明确赋值,但在另一个中不被视为已明确赋值。 在方法if
的G
语句中,变量i
在第二个嵌入语句中已明确赋值,因为表达式(i = y)
的执行总是在该嵌入语句的执行之前。 相反,变量i
在第一个嵌入语句中未明确赋值,因为x >= 0
可能已测试为 true,导致变量i
未赋值。结束示例
9.4.4.28 ! 表达式
对于以下形式的表达式 expr:
! «expr_operand»
- expr_operand 之前 v 的明确赋值状态与 expr 之前 v 的明确赋值状态相同。
-
expr 之后 v 的明确赋值状态由以下确定:
- 如果
v
之后 的状态为已明确赋值,则v
之后 的状态为已明确赋值。 - 否则,如果
v
之后 的状态为“在 false 表达式之后明确赋值”,则v
之后 的状态为“在 true 表达式之后明确赋值”。 - 否则,如果
v
之后 的状态为“在 true 表达式之后明确赋值”,则 expr 之后 v 的状态为“在 false 表达式之后明确赋值”。 - 否则,
v
之后 的状态未明确赋值。
- 如果
9.4.4.29 ?? 表达式
对于以下形式的表达式 expr:
«expr_first» ?? «expr_second»
- expr_first 之前 v 的明确赋值状态与 expr 之前 v 的明确赋值状态相同。
- expr_second 之前 v 的明确赋值状态与 expr_first 之后 v 的明确赋值状态相同。
- expr 之后 v 的明确赋值语句由以下确定:
9.4.4.30 ?: 表达式
对于以下形式的表达式 expr:
«expr_cond» ? «expr_true» : «expr_false»
- expr_cond 之前 v 的明确赋值状态与 expr 之前 v 的状态相同。
- 如果 expr_cond 之后 v 的状态为已明确赋值或“在 true 表达式之后明确赋值”,则 expr_true 之前 v 的明确赋值状态为已明确赋值。
- 如果 expr_cond 之后 v 的状态为已明确赋值或“在 false 表达式之后明确赋值”,则 expr_false 之前 v 的明确赋值状态为已明确赋值。
- expr 之后 v 的明确赋值状态由以下确定:
9.4.4.31 匿名函数
对于具有主体(块或表达式)body 的 lambda_expression 或 anonymous_method_expressionexpr:
- 参数的明确赋值状态与命名方法的参数相同(§9.2.6、§9.2.7、§9.2.8)。
- body 之前外部变量 v 的明确赋值状态与 expr 之前 v 的状态相同。 也就是说,外部变量的明确赋值状态从匿名函数的上下文继承。
- expr 之后外部变量 v 的明确赋值状态与 expr 之前 v 的状态相同。
示例:示例
class A { delegate bool Filter(int i); void F() { int max; // Error, max is not definitely assigned Filter f = (int n) => n < max; max = 5; DoWork(f); } void DoWork(Filter f) { ... } }
由于在声明匿名函数的位置 max 未明确赋值,因此会生成编译时错误。
结束示例
示例:示例
class A { delegate void D(); void F() { int n; D d = () => { n = 1; }; d(); // Error, n is not definitely assigned Console.WriteLine(n); } }
也会生成编译时错误,因为匿名函数中对
n
的赋值对匿名函数外部n
的明确赋值状态没有影响。结束示例
9.4.4.32 Throw 表达式
对于以下形式的表达式 expr:
throw
thrown_expr
- thrown_expr 之前 v 的明确赋值状态与 expr 之前 v 的状态相同。
- expr 之后 v 的明确赋值状态为“已明确赋值”。
9.4.4.33 本地函数中变量的规则
本地函数在其父方法的上下文中进行分析。 对于本地函数,有两个重要的控制流路径:函数调用和委托转换。
每个本地函数主体的明确赋值是针对每个调用站点单独定义的。 在每次调用时,如果本地函数捕获的变量在调用点已明确赋值,则这些变量被视为已明确赋值。 此时存在到本地函数主体的控制流路径,并被视为可访问。 调用本地函数后,在每个离开函数的控制点(return
语句、yield
语句、await
表达式)都已明确赋值的捕获变量在调用位置之后被视为已明确赋值。
委托转换有到本地函数主体的控制流路径。 如果捕获的变量在转换之前已明确赋值,则对于主体而言,这些变量已明确赋值。 本地函数分配的变量在转换之后不被视为已分配。
注意:以上意味着在每次本地函数调用或委托转换时,都会重新分析主体的明确赋值。 编译器不需要在每次调用或委托转换时重新分析本地函数的主体。 实现必须产生与该描述等效的结果。 尾注
示例:以下示例演示了本地函数中捕获变量的明确赋值。 如果本地函数在写入捕获的变量之前读取该变量,则在调用本地函数之前,该捕获的变量必须已明确赋值。 本地函数
F1
读取s
而不分配它。 如果在F1
已明确赋值之前调用s
,则会出错。F2
在读取i
之前对其进行分配。 可以在i
已明确赋值之前调用它。 此外,可以在F3
之后调用F2
,因为s2
在F2
中已明确赋值。void M() { string s; int i; string s2; // Error: Use of unassigned local variable s: F1(); // OK, F2 assigns i before reading it. F2(); // OK, i is definitely assigned in the body of F2: s = i.ToString(); // OK. s is now definitely assigned. F1(); // OK, F3 reads s2, which is definitely assigned in F2. F3(); void F1() { Console.WriteLine(s); } void F2() { i = 5; // OK. i is definitely assigned. Console.WriteLine(i); s2 = i.ToString(); } void F3() { Console.WriteLine(s2); } }
结束示例
9.4.4.34 is-pattern 表达式
对于以下形式的表达式 expr:
expr_operand 是 pattern
- expr_operand 之前 v 的明确赋值状态与 expr 之前 v 的明确赋值状态相同。
- 如果变量“v”在 pattern 中声明,则 expr 之后“v”的明确赋值状态为“为 true 时已明确赋值”。
- 否则,expr 之后“v”的明确赋值状态与 expr_operand 之后“v”的明确赋值状态相同。
9.5 变量引用
variable_reference 是被归类为变量的表达式。 variable_reference 表示可访问以获取当前值和存储新值的存储位置。
variable_reference
: expression
;
注意:在 C 和 C++ 中,variable_reference 称为 lvalue。 尾注
9.6 变量引用的原子性
以下数据类型的读取和写入应具有原子性:bool
、char
、byte
、sbyte
、short
、ushort
、uint
、int
、float
和引用类型。 此外,基础类型在先前列表中的枚举类型的读取和写入也应具有原子性。 其他类型的读取和写入(包括 long
、ulong
、double
和 decimal
以及用户定义的类型)不需要具有原子性。 除了为此目的设计的库函数外,不保证原子的读取-修改-写入(例如递增或递减的情况)。
9.7 引用变量和返回
9.7.1 常规
引用变量是引用另一个变量(称为引用目标,§9.2.6)的变量。 引用变量是用 ref
修饰符声明的局部变量。
引用变量存储对其引用目标的 variable_reference (§9.5),而不是其引用目标的值。 当需要值的位置使用引用变量时,返回其引用目标的值;类似地,当引用变量是赋值的目标时,是引用目标被赋值。 引用变量所引用的变量(即其引用目标的存储的 variable_reference)可以使用 ref 赋值 (= ref
) 更改。
示例:以下示例演示了引用目标是数组元素的局部引用变量:
public class C { public void M() { int[] arr = new int[10]; // element is a reference variable that refers to arr[5] ref int element = ref arr[5]; element += 5; // arr[5] has been incremented by 5 } }
结束示例
引用返回是从按引用返回的方法 (§15.6.1) 返回的 variable_reference。 此 variable_reference 是引用返回的引用目标。
示例:以下示例演示了引用目标是数组字段元素的引用返回:
public class C { private int[] arr = new int[10]; public ref readonly int M() { // element is a reference variable that refers to arr[5] ref int element = ref arr[5]; return ref element; // return reference to arr[5]; } }
结束示例
9.7.2 Ref 安全上下文
9.7.2.1 常规
所有引用变量都遵循安全规则,确保引用变量的 ref-safe-context 不大于其引用目标的 ref-safe-context。
注意: 安全上下文 的相关概念在 (§16.4.15) 中定义,以及关联的约束。 尾注
对于任何变量,该变量的 ref-safe-context 是 variable_reference (§9.5) 对该变量有效的上下文。 引用变量的引用目标的 ref-safe-context 应至少与引用变量本身的 ref-safe-context 一样宽。
注释:编译器通过对程序文本的静态分析来确定 ref-safe-上下文。 ref-safe-context 反映变量在运行时的生存期。 尾注
有三种 ref-safe-context:
声明块:局部变量 (§9.2.9.1) 的 variable_reference 的 ref-safe-context 是该局部变量的范围 (§13.6.2),包括该范围中的任何嵌套 embedded-statement。
局部变量的 variable_reference 只有在引用变量在该变量的 ref-safe-context 内声明时,才是引用变量的有效引用目标。
function-member:在函数内,以下任何项的 variable_reference 的 ref-safe-context 为 function-member:
- 函数成员声明上的值参数 (§15.6.2.2),包括类成员函数的隐式
this
;以及 - 结构成员函数的隐式引用 (
ref
) 参数 (§15.6.2.3.3)this
及其字段。
ref-safe-context 为 function-member 的 variable_reference 只有在引用变量在同一函数成员中声明时才是有效引用目标。
- 函数成员声明上的值参数 (§15.6.2.2),包括类成员函数的隐式
caller-context:在函数内,以下任何项的 variable_reference 的 ref-safe-context 为 caller-context:
ref-safe-context 为 caller-context 的 variable_reference 可以是引用返回的引用目标。
这些值形成从最窄 (declaration-block) 到最宽 (caller-context) 的嵌套关系。 每个嵌套块代表不同的上下文。
示例:以下代码显示了不同 ref-safe-context 的示例。 声明显示引用目标的 ref-safe-context 是
ref
变量的初始化表达式。 示例显示引用返回的 ref-safe-context:public class C { // ref safe context of arr is "caller-context". // ref safe context of arr[i] is "caller-context". private int[] arr = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // ref safe context is "caller-context" public ref int M1(ref int r1) { return ref r1; // r1 is safe to ref return } // ref safe context is "function-member" public ref int M2(int v1) { return ref v1; // error: v1 isn't safe to ref return } public ref int M3() { int v2 = 5; return ref arr[v2]; // arr[v2] is safe to ref return } public void M4(int p) { int v3 = 6; // context of r2 is declaration-block, // ref safe context of p is function-member ref int r2 = ref p; // context of r3 is declaration-block, // ref safe context of v3 is declaration-block ref int r3 = ref v3; // context of r4 is declaration-block, // ref safe context of arr[v3] is caller-context ref int r4 = ref arr[v3]; } }
结束示例。
示例:对于
struct
类型,隐式this
参数作为引用参数传递。 作为 function-member 的struct
类型字段的 ref-safe-context 防止通过引用返回这些字段。 此规则防止以下代码:public struct S { private int n; // Disallowed: returning ref of a field. public ref int GetN() => ref n; } class Test { public ref int M() { S s = new S(); ref int numRef = ref s.GetN(); return ref numRef; // reference to local variable 'numRef' returned } }
结束示例。
9.7.2.2 局部变量 ref 安全上下文
对于局部变量 v
:
- 如果
v
是引用变量,其 ref 安全上下文与其初始化表达式的 ref 安全上下文相同。 - 否则,其 ref-safe-context 为 declaration-block。
9.7.2.3 参数 ref 安全上下文
对于参数 p
:
- 如果
p
是引用或输入参数,其 ref-safe-context 为 caller-context。 如果p
是输入参数,它不能作为可写ref
返回,但可以作为ref readonly
返回。 - 如果
p
是输出参数,其 ref-safe-context 为 caller-context。 - 否则,如果
p
是结构类型的this
参数,其 ref-safe-context 为 function-member。 - 否则,该参数是值参数,其 ref-safe-context 为 function-member。
9.7.2.4 字段 ref 安全上下文
对于指定字段引用的变量 e.F
:
- 如果
e
是引用类型,其 ref-safe-context 为 caller-context。 - 否则,如果
e
是值类型,其 ref-safe-context 与e
的 ref-safe-context 相同。
9.7.2.5 运算符
条件运算符 (§12.18)、c ? ref e1 : ref e2
和引用赋值运算符 = ref e
(§12.21.1) 以引用变量作为操作数,并生成引用变量。 对于这些运算符,结果的 ref-safe-context 是所有 ref
操作数的 ref-safe-context 中最窄的上下文。
9.7.2.6 函数调用
对于从按引用返回的函数调用产生的变量 c
,其 ref-safe-context 是以下上下文中最窄的:
- caller-context。
- 所有
ref
、out
和in
参数表达式(不包括接收者)的 ref-safe-context。 - 对于每个输入参数,如果存在对应的作为变量的表达式,且变量的类型与参数的类型之间存在标识转换,则为变量的 ref-safe-context,否则为最近的封闭上下文。
- 所有参数表达式(包括接收方)的安全上下文(§16.4.15)。
示例:最后一个项目符号对于处理诸如以下代码是必要的
ref int M2() { int v = 5; // Not valid. // ref safe context of "v" is block. // Therefore, ref safe context of the return value of M() is block. return ref M(ref v); } ref int M(ref int p) { return ref p; }
结束示例
属性调用和索引器调用(get
或 set
)按上述规则被视为基础访问器的函数调用。 本地函数调用是函数调用。
9.7.2.7 值
值的 ref 安全上下文是最近的封闭上下文。
注意:这发生在诸如
M(ref d.Length)
的调用中,其中d
是dynamic
类型。 它也与对应于输入参数的参数一致。 尾注
9.7.2.8 构造函数调用
调用构造函数的 new
表达式遵循与被视为返回正在构造的类型的方法调用 (§9.7.2.6) 相同的规则。
9.7.2.9 引用变量的限制
- lambda 表达式或本地函数不得捕获引用参数、输出参数、输入参数、
ref
局部变量以及ref struct
类型的参数或局部变量。 - 迭代器方法或
ref struct
方法的实参不得是引用参数、输出参数、输入参数以及async
类型的参数。 -
ref
语句或ref struct
表达式的位置的上下文中不得有yield return
局部变量以及await
类型的局部变量。 - 对于 ref 重新赋值
e1 = ref e2
,e2
的 ref-safe-context 应至少与 的e1
一样宽。 - 对于 ref return 语句
return ref e1
,e1
的 ref-safe-context 应为 caller-context。