9 个变量
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]
是值参数,b
a
是引用参数,c
是输出参数,d
是输入参数,也是i
局部变量。 end 示例
9.2.2 静态变量
使用 static
修饰符声明的字段是静态变量。 静态变量在构造函数(§15.12)的包含类型执行static
之前就存在,并且当关联的应用程序域不再存在时,该变量将停止存在。
静态变量的初始值为变量类型的默认值(§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.2 中进一步讨论。
9.2.6 参考参数
引用参数是一个引用变量(§9.7),在调用函数成员、委托、匿名函数或本地函数时存在,其引用被初始化为该调用中的参数的变量。 函数主体的执行完成时,引用参数将停止存在。 与值参数不同,不应捕获引用参数(§9.7.2.9)。
以下明确分配规则适用于引用参数。
注意:输出参数的规则不同,并在 (§9.2.7) 中介绍。 end note
- 变量在函数成员或委托调用中作为引用参数传递之前,应明确分配变量(§9.4)。
- 在函数成员或匿名函数中,最初将引用参数视为已分配。
参考参数在 §15.6.2.3.3 中进一步讨论。
9.2.7 输出参数
输出参数是一个引用变量(§9.7),在调用函数成员、委托、匿名函数或局部函数时存在,其引用被初始化为该调用中的参数的变量。 当函数主体的执行完成时,输出参数将停止存在。 与值参数不同,不应捕获输出参数(§9.7.2.9)。
以下明确分配规则适用于输出参数。
注意:引用参数的规则是不同的,并在 (§9.2.6) 中介绍。 end note
- 在函数成员或委托调用中作为输出参数传递变量之前,无需明确分配变量。
- 在函数成员或委托调用的正常完成之后,作为输出参数传递的每个变量都被视为在该执行路径中分配。
- 在函数成员或匿名函数中,输出参数最初被视为未分配。
- 函数成员、匿名函数或本地函数的每个输出参数都必须在函数成员、匿名函数或本地函数正常返回之前分配(§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 局部变量
局部变量由try_statement的local_variable_declaration、declaration_expression、foreach_statement或specific_catch_clause声明。 局部变量也可以由某些类型的 模式(§11) 声明。 对于foreach_statement,局部变量是迭代变量(§13.9.5)。 对于specific_catch_clause,局部变量是异常变量(§13.11)。 最初会考虑由 foreach_statement 或 specific_catch_clause 声明的局部变量。
local_variable_declaration可能发生在块、for_statement、switch_block或using_statement中。 declaration_expression可以作为out
argument_value发生,作为析构赋值的目标tuple_element(§12.21.2)。
局部变量的生存期是程序执行的一部分,在此期间,可以保证为其保留存储。 此生存期从与其关联的作用域的条目扩展,至少直到该作用域的执行以某种方式结束。 (输入封闭 块、调用方法或从迭代器块生成值将挂起,但不结束当前范围的执行。如果局部变量由匿名函数(§12.19.6.2)捕获,则其生存期至少会延长,直到从匿名函数创建的委托或表达式树以及引用捕获的变量的任何其他对象都有资格进行垃圾回收。 如果父范围以递归方式或迭代方式输入,则每次都会创建本地变量的新实例,并且每次计算其初始值设定项(如果有)。
注意:每次输入局部变量范围时都会实例化。 此行为对包含匿名方法的用户代码可见。 end note
注意:由foreach_statement声明的迭代变量(§13.9.5)的生存期是该语句的单个迭代。 每次迭代都会创建一个新变量。 end note
注意:局部变量的实际生存期取决于实现。 例如,编译器可能静态确定块中的局部变量仅用于该块的一小部分。 使用此分析,编译器可以生成代码,使变量的存储生存期比包含块短。
本地引用变量所引用的存储独立于该本地引用变量(§7.9)的生存期回收。
end note
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
在本地变量的作用域内,在声明符之前的文本位置引用该局部变量是一个编译时错误。
end note
9.2.9.1 放弃
放弃是没有名称的局部变量。 放弃由具有标识符的声明表达式(§12.17)引入;并且是隐式类型化(_
或var _
)或显式类型化(T _
)。_
注意:
_
是多种形式的声明的有效标识符。 end note
由于放弃没有名称,因此它所表示的变量的唯一引用是引入它的表达式。
注意:但放弃可以作为输出参数传递,从而允许相应的输出参数表示其关联的存储位置。 end note
最初未分配放弃,因此访问其值始终是错误的。
示例:
_ = "Hello".Length; (int, int, int) M(out int i1, out int i2, out int i3) { ... } (int _, var _, _) = M(out int _, out var _, out _);
该示例假定范围中没有名称
_
声明。用于
_
显示用于忽略表达式结果的简单模式的赋值。 调用M
显示元组和输出参数中可用的不同形式的放弃。end 示例
9.3 默认值
以下类别的变量会自动初始化为其默认值:
- 静态变量。
- 类实例的实例变量。
- 数组元素。
变量的默认值取决于变量的类型,并按如下所示确定:
- 对于value_type变量,默认值与value_type的默认构造函数(§8.3.3.3)计算的值相同。
- 对于reference_type变量,默认值为
null
。
注意:初始化默认值通常是通过将内存管理器或垃圾回收器初始化为全位零来初始化内存,然后再将其分配给使用。 因此,使用全位零表示 null 引用很方便。 end note
9.4 明确分配
9.4.1 常规
在函数成员或匿名函数的可执行代码中的给定位置,如果编译器可以通过特定的静态流分析(§9.4.4)证明该变量已自动初始化或至少是一个赋值的目标,则明确分配变量。
注意:非正式说明,明确分配的规则是:
- 最初分配的变量(§9.4.2)始终被视为绝对分配。
- 如果导致该位置的所有可能执行路径至少包含以下一个,则最初未分配的变量(§9.4.3)将被视为在给定位置分配:
- 一个简单的赋值(§12.21.2),其中变量是左操作数。
- 将变量作为输出参数传递的调用表达式(§12.8.9)或对象创建表达式(§12.8.16.2)。
- 对于局部变量,包含变量初始值设定项的变量(§13.6.2)的局部变量声明。
上述非正式规则的基础正式规范在 §9.4.2、 §9.4.3 和 §9.4.4 中介绍。
end note
struct_type变量的实例变量的明确赋值状态单独和统一跟踪。 除了 §9.4.2、§9.4.3 和 §9.4.4 中所述的规则,以下规则适用于struct_type变量及其实例变量:
- 如果实例变量包含 struct_type 变量被视为明确分配,则被视为绝对分配实例变量。
- 如果每个实例变量都被视为明确分配,则会将struct_type变量视为绝对分配。
在以下上下文中,明确分配是一项要求:
应在获取变量值的每个位置明确分配变量。
注意:这可确保永远不会发生未定义的值。 end note
表达式中变量的出现被视为获取变量的值,除非
- 变量是简单赋值左操作数,
- 变量作为输出参数传递,或
- 该变量是一个 struct_type 变量,作为成员访问的左操作数发生。
变量应在作为引用参数传递的每个位置明确分配变量。
注意:这可确保调用的函数成员可以考虑最初分配的引用参数。 end note
变量应在作为输入参数传递的每个位置明确分配变量。
注意:这可确保调用的函数成员可以考虑最初分配的输入参数。 end note
函数成员的所有输出参数都必须在函数成员返回的每个位置(通过返回语句或通过到达函数成员体末尾的执行)进行分配。
注意:这可确保函数成员不会在输出参数中返回未定义的值,从而使编译器能够考虑将变量作为等效于对变量赋值的输出参数的函数成员调用。 end note
this
在实例构造函数返回的每个位置,应明确分配struct_type实例构造函数的变量。
9.4.2 最初分配的变量
以下类别的变量被归类为最初分配的变量:
- 静态变量。
- 类实例的实例变量。
- 最初分配的结构变量的实例变量。
- 数组元素。
- 值参数。
- 引用参数。
- 输入参数。
- 在子句或语句中
catch
声明的foreach
变量。
9.4.3 最初未分配的变量
以下类别的变量被归类为最初未分配的变量:
- 最初未分配的结构变量的实例变量。
- 输出参数,包括
this
没有构造函数初始值设定项的结构实例构造函数的变量。 - 局部变量(子句或
foreach
语句中catch
声明的变量除外)。
9.4.4 用于确定明确分配的精确规则
9.4.4.1 常规
为了确定明确分配每个已用变量,编译器应使用与此子引用中所述的变量等效的进程。
编译器处理具有一个或多个初始未分配变量的每个函数成员的主体。 对于每个最初未分配的变量 v,编译器确定函数成员中以下每个点的 v 的明确赋值状态:
- 在每个语句的开头
- 在每个语句的终点(§13.2)
- 在将控件传输到另一语句或语句终点的每个弧上
- 在每个表达式的开头
- 在每个表达式的末尾
v 的明确赋值状态可以是:
- 绝对分配。 这表示,在所有可能的控制流中, v 已分配一个值。
- 不是绝对分配的。 对于类型
bool
表达式末尾的变量的状态,未明确分配的变量的状态(但不一定)属于以下子状态之一:- 在 true 表达式之后明确分配。 此状态指示 如果布尔表达式的计算结果为 true,则绝对分配 v ,但如果布尔表达式的计算结果为 false,则不一定分配 v。
- 在 false 表达式之后明确分配。 此状态指示 如果布尔表达式的计算结果为 false,则绝对分配 v ,但如果布尔表达式的计算结果为 true,则不一定分配 v。
以下规则控制变量 v 在每个位置的状态。
9.4.4.2 语句的一般规则
- v 在函数成员正文的开头未明确分配。
- 任何其他语句开头的 v 的明确赋值状态是通过检查针对该语句开头的所有控制流传输的 v 的明确赋值状态来确定的。 如果(且仅当) v 在所有这些控制流传输中明确分配,则 v 在语句的开头明确分配。 可能的控制流传输集与检查语句可访问性(§13.2)的方式相同。
- v 在一个
block
、checked
、、while
foreach
do
for
if
unchecked
、lock
或using
switch
语句的终点的明确赋值状态是通过检查 v 的所有控制流传输的明确赋值状态来确定的,该状态以该语句的终点为目标。 如果 v 在所有这些控制流传输中明确分配,则 v 绝对在语句的终点分配。 否则, 在语句的终点未明确分配 v 。 可能的控制流传输集与检查语句可访问性(§13.2)的方式相同。
注意:由于没有不可访问语句的控制路径, 因此在任何不可访问语句的开头都明确分配 v 。 end note
9.4.4.3 阻止语句、已选中和未选中的语句
控制转移至块中语句列表的第一个语句(或块的终点(如果语句列表为空)的 v 的明确赋值状态与块前 v 的明确赋值语句相同,checked
或unchecked
语句。
9.4.4.4 表达式语句
对于由表达式 expr 组成的表达式语句 stmt:
- v 在 expr 开始时与 stmt 的开头具有相同的明确分配状态。
- 如果在 expr 末尾明确分配了 v,则它肯定在 stmt 的终点分配;否则,它不会在 stmt 的终点进行明确分配。
9.4.4.5 声明语句
- 如果 stmt 是没有初始值设定项的声明语句,则 v 在 stmt 的终点具有与 stmt 开头相同的明确赋值状态。
- 如果 stmt 是具有初始值设定项的声明语句,则 v 的明确赋值状态将确定为一个语句列表,每个声明都有一个赋值语句(按声明顺序)。
9.4.4.6 If 语句
对于窗体的语句 stmt :
if ( «expr» ) «then_stmt» else «else_stmt»
- v 在 expr 开始时与 stmt 的开头具有相同的明确分配状态。
- 如果 v 在 expr 结束时明确分配,则在控制流传输中明确分配给then_stmt,如果不存在其他子句,则将其分配给else_stmt或 stmt 的终点。
- 如果 v 在 expr 结束时的状态为“在 true 表达式之后明确分配”,则控制流传输中明确分配给then_stmt,并且如果没有其他子句,则不会在控制流传输上明确分配给else_stmt或 stmt 的终点。
- 如果 v 在 expr 结束时将状态“明确分配为 false 表达式”,则会在控制流传输到 else_stmt 上明确分配该状态,并且不会将控制流传输分配给then_stmt。 它绝对是在 stmt 的终点分配的,前提是它绝对是在then_stmt的终点分配的。
- 否则,v 在控制流传输中未明确分配给then_stmt或else_stmt,或者如果没有其他子句,则被视为未分配给 stmt 的终点。
9.4.4.7 Switch 语句
switch
对于具有控制表达式 expr 的语句 stmt:
expr 开始时 v 的明确赋值状态与 stmt 开头的 v 状态相同。
案例的 guard 子句开头 v 的明确赋值状态为
- 如果 v 是switch_label中声明的模式变量:“绝对已分配”。
- 如果包含该 guard 子句(§13.8.3)的开关标签不可访问:“明确分配”。
- 否则,v 的状态与 expr 后 v 的状态相同。
示例:如果无法访问代码中访问了未分配的变量,则第二个规则无需编译器发出错误。 b 的状态在无法访问的开关标签
case 2 when b
中“明确分配”。bool b; switch (1) { case 2 when b: // b is definitely assigned here. break; }
end 示例
控制流传输到可访问的交换机块语句列表中的 v 的明确赋值状态为
- 如果控制传输是由于“goto case”或“goto default”语句导致的,则 v 的状态与该“goto”语句开头的状态相同。
- 如果控制传输是由于
default
开关的标签引起的,则 v 的状态与 expr 后 v 的状态相同。 - 如果控制传输是由于无法访问的开关标签导致的,则 v 的状态是“绝对分配的”。
- 如果控制传输是由于具有 guard 子句的可访问开关标签导致的,则 v 的状态与 guard 子句后 v 的状态相同。
- 如果控制传输是由于没有 guard 子句的可访问开关标签导致的,则 v 的状态为
- 如果 v 是switch_label中声明的模式变量:“绝对已分配”。
- 否则,v 的状态与 expr 后的 v 统计信息相同。
这些规则的结果是,如果switch_label中声明的模式变量不是其节中唯一可访问的开关标签,则会在其开关节的语句中“不明确分配”。
示例:
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; … } }
end 示例
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
将语句转换为goto
面向标签LLoop
的语句的语句的语句for
。 如果语句中省略for
了for_condition,则对确定赋值收益的计算就好像在上述扩展中用 true 替换了for_condition。
9.4.4.11 中断、继续和 goto 语句
v 在控制流传输中由一个break
或goto
continue
语句引起的明确赋值状态与语句开头的 v 的明确赋值状态相同。
9.4.4.12 Throw 语句
对于窗体的语句 stmt :
throw «expr» ;
expr 开始时 v 的明确赋值状态与 stmt 开始时 v 的明确赋值状态相同。
9.4.4.13 返回语句
对于窗体的语句 stmt :
return «expr» ;
- expr 开始时 v 的明确赋值状态与 stmt 开始时 v 的明确赋值状态相同。
- 如果 v 是输出参数,则应明确分配它:
- expr 之后
- 或位于
return
包含语句的--finally
try
-finally
catch
try
块的finally
末尾。
对于窗体的语句 stmt :
return ;
- 如果 v 是输出参数,则应明确分配它:
- stmt 之前
- 或位于
return
包含语句的--finally
try
-finally
catch
try
块的finally
末尾。
9.4.4.14 Try-catch 语句
对于窗体的语句 stmt :
try «try_block»
catch ( ... ) «catch_block_1»
...
catch ( ... ) «catch_block_n»
- try_block开头 v 的明确赋值状态与 stmt 开始时 v 的明确赋值状态相同。
- catch_block_i(对于任何 i)开头的 v 的明确赋值状态与 stmt 开始时 v 的明确赋值状态相同。
- 如果在try_block的终点和每个catch_block_i(对于每一个从 1 到 n),则绝对分配 v 的明确赋值状态( 如果)v 在try_block的终点和每个catch_block_i(对于每一个 1 到 n)。
9.4.4.15 Try-finally 语句
对于窗体的语句 stmt :
try «try_block» finally «finally_block»
- try_block开头 v 的明确赋值状态与 stmt 开始时 v 的明确赋值状态相同。
- finally_block开始时 v 的明确赋值状态与 stmt 开始时 v 的明确赋值状态相同。
- 如果(且仅当以下至少一个为 true),则绝对分配 v 在 stmt 的终点处的明确赋值状态:
- v 肯定在try_block的 终点分配
- v 绝对在finally_block的 终点分配
如果控制流传输(例如goto
语句)在try_block内开始,并在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 } }
end 示例
9.4.4.17 Foreach 语句
对于窗体的语句 stmt :
foreach ( «type» «identifier» in «expr» ) «embedded_statement»
- expr 开始时 v 的明确赋值状态与 stmt 开头的 v 状态相同。
- 控制流传输到embedded_statement或 stmt 终点的 v 的明确分配状态与 expr 结束时 v 的状态相同。
9.4.4.18 Using 语句
对于窗体的语句 stmt :
using ( «resource_acquisition» ) «embedded_statement»
- resource_acquisition开头的 v 的明确赋值状态与 stmt 开头的 v 状态相同。
- 控制流传输到embedded_statement的 v 的明确分配状态与resource_acquisition结束时 v 的状态相同。
9.4.4.19 Lock 语句
对于窗体的语句 stmt :
lock ( «expr» ) «embedded_statement»
- expr 开始时 v 的明确赋值状态与 stmt 开头的 v 状态相同。
- 控制流传输到embedded_statement的 v 的明确分配状态与 expr 结束时 v 的状态相同。
9.4.4.20 Yield 语句
对于窗体的语句 stmt :
yield return «expr» ;
- expr 开始时 v 的明确赋值状态与 stmt 开头的 v 状态相同。
- stmt 末尾的 v 的明确赋值状态与 expr 末尾的 v 状态相同。
语句 yield break
对明确赋值状态没有影响。
9.4.4.21 常量表达式的常规规则
以下内容适用于任何常量表达式,并优先于可能适用的以下部分中的任何规则:
对于具有值的 true
常量表达式:
- 如果 v 在表达式之前明确分配,则表达式之后将明确分配 v。
- 否则 ,v 在表达式后“在 false 表达式后明确分配”。
示例:
int x; if (true) {} else { Console.WriteLine(x); }
end 示例
对于具有值的 false
常量表达式:
- 如果 v 在表达式之前明确分配,则表达式之后将明确分配 v。
- 否则 ,v 在表达式后“在 true 表达式后明确分配”。
示例:
int x; if (false) { Console.WriteLine(x); }
end 示例
对于所有其他常量表达式,表达式后的 v 的明确赋值状态与表达式之前的 v 的明确赋值状态相同。
简单表达式的常规规则 9.4.4.22
以下规则适用于这些类型的表达式:文本(§12.8.2)、简单名称(§12.8.4)、成员访问表达式(§12.8.7)、非索引基访问表达式(§12) .8.14、typeof
表达式(§12.8.17)、默认值表达式(§12.8.20)、nameof
表达式(§12.8.22)和声明表达式(§12.17)。
- 此类表达式末尾的 v 的明确赋值状态与表达式开头的 v 的明确赋值状态相同。
9.4.4.23 带有嵌入表达式的表达式的常规规则
以下规则适用于这些类型的表达式:带括号表达式(§12.8.5)、元组表达式(§12.8.6)、元素访问表达式(§12.8.11)、带索引的基本访问表达式(§12.8.14)、递增和递减表达式(§12.8.15) §12.9.6)、强制转换表达式(§12.9.7)、一元+
、-
、、表达式 *
~
、二进制+
、-
、、<<
*
/
>>
%
、、 <=
<
>
、、>=
、!=
==
、is
、as
、&
、|
表达式^
(§12.10、 §12.11、§12.12、§12.13、复合赋值表达式(§12.21.4)和unchecked
表达式(§12.8.19)、checked
数组和委托创建表达式(§12.8.16)和await
表达式(§12.9.8)。
其中每个表达式都有一个或多个按固定顺序无条件计算的子表达式。
示例:二进制
%
运算符计算运算符的左侧,然后计算右侧。 索引操作计算索引表达式,然后按从左到右的顺序计算每个索引表达式。 end 示例
对于具有子表达式 expr₁、exprー、...、exprₓ 的表达式 expr,按以下顺序计算:
- expr₁ 开始时 v 的明确赋值状态与 expr 开始时的明确赋值状态相同。
- expri(i 大于 1)开头的 v 的明确赋值状态与 expri₋₁ 结束时的明确赋值状态相同。
- 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 状态相同。
- 对于每个参数 argi,argi 之后 v 的明确赋值状态由正常表达式规则确定,忽略任何
in
或out
ref
修饰符。 - 对于任何 i 大于 1 的参数 argi,argi 之前的 v 的明确赋值状态与 argi₋₁ 后的 v 状态相同。
- 如果变量 v 作为参数传递(即任何参数中的“out v”形式的参数),则绝对分配 expr 后的 v 状态。
out
否则,expr 后的 v 状态与 argₓ 后的 v 状态相同。 - 对于数组初始值设定项(§12.8.16.5)、对象初始值设定项(§12.8.16.3)、集合初始值设定项(§12.8.16.4)和匿名对象初始值设定项(§12.8.16.7),明确赋值状态由定义这些构造的扩展确定。
9.4.4.25 简单赋值表达式
让表达式中的一组赋值目标定义如下:
- 如果 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 } }
在计算为第二个简单赋值左侧后
arr[x = 1]
,该变量x
被视为明确分配。end 示例
9.4.4.26 && 表达式
对于窗体的表达式 expr :
«expr_first» && «expr_second»
- expr_first之前的 v 的明确赋值状态与 expr 之前的 v 的明确赋值状态相同。
- 在expr_second之前,v 的明确赋值状态在expr_first之后被明确分配或“在 true 表达式之后明确分配”时,才会分配 v 的状态。 否则,不会明确分配它。
- 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
未分配。end 示例
9.4.4.27 ||表达 式
对于窗体的表达式 expr :
«expr_first» || «expr_second»
- expr_first之前的 v 的明确赋值状态与 expr 之前的 v 的明确赋值状态相同。
- 在expr_second之前,v 的明确赋值状态在expr_first之后被明确分配或“在 true 表达式之后明确分配”时,才会分配 v 的状态。 否则,不会明确分配它。
- 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 表达式后明确分配”,并且 v 之后expr_秒后的状态是“在 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
未分配。end 示例
9.4.4.28 ! 表达式
对于窗体的表达式 expr :
! «expr_operand»
- expr_operand之前的 v 的明确赋值状态与 expr 之前的 v 的明确赋值状态相同。
- expr 后 v 的明确赋值状态由以下决定:
- 如果明确分配了expr_operand后的状态
v
,则明确分配 expr 之后的状态v
。 - 否则,如果expr_operand后的状态“在 false 表达式后明确分配”,则 expr 之后的状态
v
v
为“在 true 表达式后明确分配”。 - 否则,如果expr_operand后的状态
v
“在 true 表达式后明确分配”,则 expr 之后的 v 状态将“在 false 表达式后明确分配”。 - 否则,不明确分配 expr 之后的状态
v
。
- 如果明确分配了expr_operand后的状态
9.4.4.29 ?? 表达式
对于窗体的表达式 expr :
«expr_first» ?? «expr_second»
- expr_first之前的 v 的明确赋值状态与 expr 之前的 v 的明确赋值状态相同。
- expr_second之前的 v 的明确赋值状态与expr_first后的 v 的明确赋值状态相同。
- expr 之后 v 的明确赋值语句由以下决定:
- 如果expr_first是具有值的
null
常量表达式(§12.23),则 expr 后的 v 状态与expr_second后 v 的状态相同。 - 否则,expr 后的 v 状态与expr_first后 v 的明确赋值状态相同。
- 如果expr_first是具有值的
9.4.4.30 ?: 表达式
对于窗体的表达式 expr :
«expr_cond» ? «expr_true» : «expr_false»
- expr_cond之前的 v 的明确赋值状态与 expr 之前的 v 状态相同。
- 如果expr_cond后 v 的状态已明确分配或“在 true 表达式之后明确分配 v”,则绝对分配 v 之前expr_true的状态。
- 如果expr_cond之后的 v 状态已明确分配或“在 false 表达式后明确分配 v”,则绝对分配 v 之前expr_false的状态。
- expr 后 v 的明确赋值状态由以下决定:
9.4.4.31 匿名函数
对于具有正文(块或表达式)正文的lambda_expression或anonymous_method_expression expr:
- 参数的明确赋值状态与命名方法的参数 (§9.2.6, §9.2.7, §9.2.8) 的参数相同。
- 正文之前的外部变量 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。
end 示例
示例:示例
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
明确赋值状态。end 示例
9.4.4.32 引发表达式
对于窗体的表达式 expr :
throw
thrown_expr
- thrown_expr之前的 v 的明确赋值状态与 expr 之前的 v 状态相同。
- expr 之后 v 的明确分配状态是“绝对分配的”。
9.4.4.33 局部函数中变量的规则
本地函数在父方法的上下文中进行分析。 本地函数有两个控制流路径:函数调用和委托转换。
为每个调用站点单独定义每个本地函数正文的明确赋值。 在每个调用中,如果局部函数捕获的变量在调用点被明确分配,则会将其视为明确分配。 此时,本地函数体也存在控制流路径,被视为可访问。 调用本地函数后,捕获的变量在离开函数的每个控制点(return
语句、语句、 yield
await
表达式)都被视为在调用位置之后明确分配的变量。
委托转换具有到本地函数正文的控制流路径。 如果已捕获的变量在转换之前明确分配了捕获的变量,则会为正文分配这些变量。 本地函数分配的变量在转换后不被视为分配。
注意:上述表示将重新分析正文,以便在每次本地函数调用或委托转换时进行明确赋值。 在每次调用或委托转换时,不需要编译器重新分析本地函数的主体。 实现必须生成与该说明等效的结果。 end note
示例:以下示例演示本地函数中捕获变量的明确赋值。 如果本地函数在写入之前读取捕获的变量,则必须在调用本地函数之前明确分配捕获的变量。 本地函数
F1
在没有分配的情况下读取s
。 如果在F1
明确分配之前s
调用,则这是一个错误。F2
在i
读取它之前分配。 在明确分配之前i
,可以调用它。 此外,可以调用后F2
,F3
因为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); } }
end 示例
9.4.4.34 is-pattern 表达式
对于窗体的表达式 expr :
expr_operand为模式
- expr_operand之前的 v 的明确赋值状态与 expr 之前的 v 的明确赋值状态相同。
- 如果变量“v”在模式中声明,则 expr 后“v”的明确赋值状态为“在 true 时明确赋值”。
- 否则,expr 后“v”的明确赋值状态与expr_operand后“v”的明确赋值状态相同。
9.5 变量引用
variable_reference是分类为变量的表达式。 variable_reference表示可以同时访问的存储位置来提取当前值和存储新值。
variable_reference
: expression
;
注意:在 C 和 C++ 中, variable_reference 称为 左值。 end note
9.6 变量引用的原子性
以下数据类型的读取和写入应为原子类型:bool
、、、short
byte
char
ushort
uint
sbyte
、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 } }
end 示例
引用返回是从 returns-by-ref 方法(§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]; } }
end 示例
9.7.2 Ref 安全上下文
9.7.2.1 常规
所有引用变量都遵循安全规则,以确保引用变量的 ref-safe-context 不大于引用的 ref-safe-context。
注意:安全上下文的相关概念在 (§16.4.12) 中定义,以及关联的约束。 end note
对于任何变量,该变量的 ref-safe-context 是该 变量variable_reference (§9.5)有效的上下文。 引用变量的引用应具有与引用变量本身的 ref-safe-context 一样宽的 ref-safe-context。
注意:编译器通过对程序文本的静态分析来确定 ref-safe-context。 ref-safe-context 反映运行时变量的生存期。 end note
有三个 ref-safe-contexts:
declaration-block:对局部变量(§9.2.9)variable_reference的 ref-safe-context 是局部变量的范围(§13.6.2),包括该作用域中的任何嵌套嵌入式语句。
仅当引用变量在引用变量的 ref-safe-context 中声明引用变量时,本地变量的variable_reference是引用变量的有效引用。
function-member:在函数中, 以下任一函数variable_reference 具有 function-member 的 ref-safe-context:
- 函数成员声明上的值参数 (§15.6.2.2.2),包括类成员函数的隐式
this
; - 结构成员函数及其字段的隐式引用 (
ref
)参数 (§15.6.2.3.3.3)。this
仅当引用变量在同一函数成员中声明引用变量时,具有函数成员的 ref-safe-context 的variable_reference才有效引用。
- 函数成员声明上的值参数 (§15.6.2.2.2),包括类成员函数的隐式
caller-context:在函数 中,对以下任一项variable_reference 具有调用方上下文的 ref-safe-context:
- 引用参数(§9.2.6)而不是结构成员函数的隐式
this
参数; - 此类参数的成员字段和元素;
- 类类型的参数的成员字段;和
- 数组类型的参数元素。
- 引用参数(§9.2.6)而不是结构成员函数的隐式
具有调用方上下文的 ref-safe-context 的variable_reference可以是引用返回的引用。
这些值构成从最窄(声明块)到最宽(调用方上下文)的嵌套关系。 每个嵌套块都表示不同的上下文。
示例:下面的代码演示了不同 ref-safe-contexts 的示例。 声明显示引用的 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]; } }
end 示例。
示例:对于
struct
类型,隐式this
参数作为引用参数传递。 作为函数成员的字段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 } }
end 示例。
9.7.2.2 局部变量 ref 安全上下文
对于局部变量 v
:
- 如果
v
为引用变量,则其 ref-safe-context 与初始化表达式的 ref-safe-context 相同。 - 否则,其 ref-safe-context 为 declaration-block。
9.7.2.3 参数 ref 安全上下文
对于参数 p
:
- 如果是
p
引用或输入参数,则其 ref-safe-context 是调用方上下文。 如果p
为输入参数,则无法将其作为可ref
写返回,但可以返回为ref readonly
。 - 如果
p
为输出参数,则其 ref-safe-context 是调用方上下文。 - 否则,如果是
p
this
结构类型的参数,则其 ref-safe-context 是函数成员。 - 否则,该参数是一个值参数,其 ref-safe-context 是函数成员。
9.7.2.4 字段 ref 安全上下文
对于指定对字段的引用的变量, e.F
:
- 如果
e
引用类型为引用类型,则其 ref-safe-context 是调用方上下文。 - 否则,如果
e
值为值类型,则其 ref-safe-context 与 ref-safe-contexte
相同。
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 函数调用
对于由 ref-returning 函数调用生成的变量 c
,其 ref-safe-context 是下列上下文中最窄的一个:
- 调用方上下文。
- 所有
ref
out
参数表达式in
(不包括接收方)的 ref-safe-context。 - 对于每个输入参数,如果有一个相应的表达式是变量,并且变量的类型与参数的类型之间存在标识转换,则变量的 ref-safe-context,否则为最近的封闭上下文。
- 所有参数表达式(包括接收方)的安全上下文(§16.4.12)。
示例:处理代码所需的最后一个项目符号,例如
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; }
end 示例
属性调用和索引器调用(或get
set
)被视为上述规则对基础访问器的函数调用。 本地函数调用是函数调用。
9.7.2.7 值
值的 ref-safe-context 是最近的封闭上下文。
注意:这发生在调用中,例如
M(ref d.Length)
类型为何处d
dynamic
。 它还与与输入参数对应的参数一致。 end note
9.7.2.8 构造函数调用
new
调用构造函数的表达式遵循与方法调用(§9.7.2.6)相同的规则,该规则被视为返回所构造的类型。
9.7.2.9 引用变量的限制
- 无论是引用参数、输出参数、输入参数,还是
ref
本地参数,或者类型的参数或局部ref struct
参数,都不应由 lambda 表达式或本地函数捕获。 - 无论是引用参数、输出参数、输入参数,还是类型参数
ref struct
,都不应是迭代器方法或async
方法的参数。 - 类型
ref
的区域或局部ref struct
区域都不应位于语句或await
表达式的yield return
上下文中。 - 对于 ref 重新分配
e1 = ref e2
,其 ref-safe-contexte2
应至少与 ref-safe-contexte1
一样宽。 - 对于 ref return 语句
return ref e1
,ref-safe-contexte1
应为调用方上下文。