1. 语句
C# 提供各种语句。使用过 C 和 C++ 编程的开发人员熟悉其中大多数语句。
statement:
labeled-statement
declaration-statement
embedded-statement
embedded-statement:
block
empty-statement
expression-statement
selection-statement
iteration-statement
jump-statement
try-statement
checked-statement
unchecked-statement
lock-statement
using-statement
yield-statement
embedded-statement 非终结符用于在其他语句内出现的语句。使用 embedded-statement(而非 statement)便不需要在这些上下文中使用声明语句和标记语句。下面的示例
void F(bool b) {
if (b)
int i = 44;
}
将导致编译时错误,原因是 if 语句的 if 分支要求 embedded-statement 而不是 statement。若允许执行上述代码,则声明了变量 i,却永远无法使用它。但是请注意,如果是将 i 的声明放置在一个块中,则该示例就是有效的。
1.1 结束点和可到达性
每个语句都有一个结束点 (end point)。直观地讲,语句的结束点是紧跟在语句后面的那个位置。复合语句(包含嵌入语句的语句)的执行规则规定了当控制到达一个嵌入语句的结束点时所采取的操作。例如,当控制到达块中某个语句的结束点时,控制就转到该块中的下一个语句。
如果执行流程可能到达某个语句,则称该语句是可到达的 (reachable)。相反,如果某个语句不可能被执行,则称该语句是不可到达的 (unreachable)。
在下面的示例中
void F() {
Console.WriteLine(“reachable”);
goto Label;
Console.WriteLine(“unreachable”);
Label:
Console.WriteLine(“reachable”);
}
第二个 Console.WriteLine 调用是不可到达的,这是因为不可能执行该语句。
如果编译器确定某个语句是不可到达的,将会报出警告。准确地说,语句不可到达不算是错误。
为了确定某个特定的语句或结束点是否可到达,编译器根据为各语句定义的可到达性规则进行控制流分析。控制流分析会考虑那些能控制语句行为的常量表达式(第 7.19 节)的值,但不考虑非常量表达式的可能值。换句话说,出于控制流分析的目的,给定类型的非常量表达式被认为具有该类型的任何可能值。
在下面的示例中
void F() {
const int i = 1;
if (i == 2)
Console.WriteLine(“unreachable”);
}
if 语句的布尔表达式是常量表达式,原因是 == 运算符的两个操作数都是常量。由于该常量表达式在编译时进行计算并产生值 false,所以 Console.WriteLine 调用被认为是不可到达的。但是,如果 i 更改为局部变量
void F() {
int i = 1;
if (i == 2)
Console.WriteLine(“reachable”);
}
则 Console.WriteLine 调用被认为是可到达的,即使它实际上永远不会被执行。
函数成员的 block 始终被认为是可到达的。通过依次计算块中各语句的可到达性规则,可以确定任何给定语句的可到达性。
在下面的示例中
void F(int x) {
Console.WriteLine(“start”);
if (x < 0)
Console.WriteLine(“negative”);
}
第二个 Console.WriteLine 的可到达性按下面的规则确定:
- 第一个 Console.WriteLine 表达式语句是可到达的,原因是 F 方法块是可到达的。
- 第一个 Console.WriteLine 表达式语句的结束点是可到达的,原因是该语句是可到达的。
- if 语句是可到达的,原因是第一个 Console.WriteLine 表达式语句的结束点是可到达的。
- 第二个 Console.WriteLine 表达式语句是可到达的,原因是 if 语句的布尔表达式没有常量值 false。
在下列两种情况下,如果某个语句的结束点是可以到达的,则会出现编译时错误:
- 由于 switch 语句不允许一个 switch 节“贯穿”到下一个 switch 节,因此如果一个 switch 节的语句列表的结束点是可到达的,则会出现编译时错误。如果发生此错误,则通常表明该处遗漏了一个 break 语句。
- 如果一个计算某个值的函数成员的块的结束点是可到达的,则会出现编译时错误。如果发生此错误,则通常表明该处遗漏了一个 return 语句。
1.2 块
block 用于在只允许使用单个语句的上下文中编写多条语句。
block:
{ statement-listopt }
block 由一个扩在大括号内的可选 statement-list(第 8.2.1 节)组成。如果没有此语句列表,则称块是空的。
块可以包含声明语句(第 8.5 节)。在一个块中声明的局部变量或常量的范围就是该块本身。
在块内,在表达式上下文中使用的名称的含义必须始终相同(第 7.6.2.1 节)。
块按下述规则执行:
- 如果块是空的,控制转到块的结束点。
- 如果块不是空的,控制转到语句列表。当(如果)控制到达语句列表的结束点时,控制转到块的结束点。
如果块本身是可到达的,则块的语句列表是可到达的。
如果块是空的或者如果语句列表的结束点是可到达的,则块的结束点是可到达的。
包含一条或多条 yield 语句(第 8.14 节)的 block 称为迭代器块。迭代器块用于以迭代器的形式实现函数成员(第 10.14 节)。某些附加限制适用于迭代器块:
- 迭代器块中出现 return 语句时会产生编译时错误(但允许 yield return 语句)。
- 迭代器块包含不安全的上下文(第 18.1 节)时将导致编译时错误。迭代器块总是定义安全的上下文,即使其定义嵌套在不安全的上下文中也如此。
1.2.1 语句列表
语句列表 (statement list) 由一个或多个顺序编写的语句组成。语句列表出现在 block(第 8.2 节)和 switch-block(第 8.7.2 节)中。
statement-list:
statement
statement-list statement
执行一个语句列表就是将控制转到该列表中的第一个语句。当(如果)控制到达某条语句的结束点时,控制将转到下一个语句。当(如果)控制到达最后一个语句的结束点时,控制将转到语句列表的结束点。
如果下列条件中至少一个为真,则语句列表中的一个语句是可到达的:
- 该语句是第一个语句且语句列表本身是可到达的。
- 前一个语句的结束点是可到达的。
- 该语句本身是一个标记语句,并且该标签已被一个可到达的 goto 语句引用。
如果列表中最后一个语句的结束点是可到达的,则语句列表的结束点是可到达的。
1.3 空语句
empty-statement 什么都不做。
empty-statement:
;
当在要求有语句的上下文中不执行任何操作时,使用空语句。
执行一个空语句就是将控制转到该语句的结束点。这样,如果空语句是可到达的,则空语句的结束点也是可到达的。
当编写一个语句体为空的 while 语句时,可以使用空语句:
bool ProcessMessage() {…}
void ProcessMessages() {
while (ProcessMessage())
;
}
此外,空语句还可以用于在块的结束符“}”前声明标签:
void F() {
…
if
(done) goto exit;
…
exit:
;
}
1.4 标记语句
labeled-statement 可以给语句加上一个标签作为前缀。标记语句可以出现在块中,但是不允许它们作为嵌入语句。
labeled-statement:
identifier :
statement
标记语句声明了一个标签,它由 identifier 来命名。标签的范围为在其中声明了该标签的整个块,包括任何嵌套块。两个同名的标签若具有重叠的范围,则会产生一个编译时错误。
标签可以在该标签的范围内被 goto 语句(第 8.9.3 节)引用。这意味着 goto 语句可以在它所在的块内转移控制,也可以将控制转到该块外部,但是永远不能将控制转入该块所含的嵌套块的内部。
标签具有自己的声明空间,并不影响其他标识符。下面的示例
int F(int x)
{
if (x >= 0) goto x;
x = -x;
x: return x;
}
是有效的,尽管它将 x 同时用作参数和标签的名称。
执行一个标记语句就是执行该标签后的那个语句。
除由正常控制流程提供的可到达性外,如果一个标签由一个可到达的 goto 语句引用,则该标记语句是可到达的。(异常:如果 goto 语句在一个包含了 finally 块的 try 中,标记语句在 try 之外,并且 finally 块的结束点不可到达,则从该 goto 语句不可到达上述标记语句。)
1.5 声明语句
declaration-statement 声明局部变量或常量。声明语句可以出现在块中,但不允许它们作为嵌入语句使用。
declaration-statement:
local-variable-declaration ;
local-constant-declaration ;
1.5.1 局部变量声明
local-variable-declaration 声明一个或多个局部变量。
local-variable-declaration:
local-variable-type
local-variable-declarators
local-variable-type:
type
var
local-variable-declarators:
local-variable-declarator
local-variable-declarators , local-variable-declarator
local-variable-declarator:
identifier
identifier = local-variable-initializer
local-variable-initializer:
expression
array-initializer
local-variable-declaration 的 local-variable-type 要么直接指定声明引入的变量的类型,要么通过标识符 var 指示应基于初始值设定项来推断该类型。此类型后接一个 local-variable-declarator 列表,其中每一项都引入一个新变量。local-variable-declarator 由一个命名变量的 identifier 组成,根据需要此 identifier 后可接一个“=”标记和一个赋予变量初始值的 local-variable-initializer。
在局部变量声明的上下文中,标识符 var 充当上下文关键字(第 2.4.3 节)。将 local-variable-type 指定为 var 且作用域内没有名为 var 的类型时,则该声明为隐式类型化局部变量声明 (implicitly typed local variable declaration),其类型从关联的初始值设定项表达式的类型推断。隐式类型化局部变量声明受到以下限制:
- local-variable-declaration 不能包含多个 local-variable-declarator。
- local-variable-declarator 必须包含一个 local-variable-initializer。
- local-variable-initializer 必须是 expression。
- 初始值设定项 expression 必须具有编译时类型。
- 初始值设定项 expression 不能引用声明的变量本身。
下面是不正确的隐式类型化局部变量声明的示例:
var x; // Error, no initializer to
infer type from
var y = {1, 2, 3}; // Error, array
initializer not permitted
var z = null; // Error, null does
not have a type
var u = x => x + 1; // Error,
anonymous functions do not have a type
var v = v++; // Error,
initializer cannot refer to variable itself
可以在表达式中通过 simple-name(第 7.6.2 节)来获取局部变量的值,还可以通过 assignment(第 7.17 节)来修改局部变量的值。在使用局部变量的每个地方必须先明确对其赋值(第 5.3 节),然后才可使用它的值。
在 local-variable-declaration 中声明的局部变量范围是该声明所在的块。在一个局部变量的 local-variable-declarator 之前的文本位置中引用该局部变量是错误的。在一个局部变量的范围内声明其他具有相同名称的局部变量或常量是编译时错误。
声明了多个变量的局部变量声明等效于多个同一类型的单个变量的声明。另外,局部变量声明中的变量初始值设定项完全对应于紧跟该声明后插入的赋值语句。
下面的示例
void F() {
int x = 1, y, z = x * 2;
}
完全对应于
void F() {
int x; x = 1;
int y;
int z; z = x * 2;
}
在隐式类型化局部变量声明中,假定所声明的局部变量的类型与用于初始化该变量的表达式的类型相同。例如:
var i = 5;
var s = “Hello”;
var d = 1.0;
var numbers = new int[] {1, 2, 3};
var orders = new Dictionary<int,Order>();
上述隐式类型化局部变量声明与下面显式类型化的声明完全等效:
int i = 5;
string s = “Hello”;
double d = 1.0;
int[] numbers = new int[] {1, 2, 3};
Dictionary<int,Order> orders = new Dictionary<int,Order>();
1.5.2 局部常量声明
local-constant-declaration 用于声明一个或多个局部常量。
local-constant-declaration:
const type constant-declarators
constant-declarators:
constant-declarator
constant-declarators , constant-declarator
constant-declarator:
identifier = constant-expression
local-constant-declaration 的 type 指定由该声明引入的常量的类型。此类型后接一个 constant-declarator 列表,其中每一项都引入一个新常量。cconstant-declarator 包含一个命名常量的 identifier,后接一个“=”标记,然后是一个对该常量赋值的 constant-expression(第 7.19 节)。
局部常量声明的 type 和 constant-expression 必须遵循与常量成员声明(第 10.4 节)一样的规则。
可以在表达式中通过 simple-name(第 7.6.2 节)来获取局部常量的值。
局部常量的范围是在其中声明了该常量的块。在局部常量的 constant-declarator 之前的文本位置中引用该局部常量是错误的。在局部常量的范围内声明其他具有相同名称的局部变量或常量是编译时错误。
声明多个常量的局部常量声明等效于多个同一类型的单个常量的声明。
1.6 表达式语句
expression-statement 用于计算所给定的表达式。由此表达式计算出来的值(如果有)被丢弃。
expression-statement:
statement-expression ;
statement-expression:
invocation-expression
object-creation-expression
assignment
post-increment-expression
post-decrement-expression
pre-increment-expression
pre-decrement-expression
await-expression
不是所有的表达式都允许作为语句使用。具体而言,不允许像 x + y 和 x == 1 这样只计算一个值(此值将被放弃)的表达式作为语句使用。
执行一个 expression-statement 就是对包含的表达式进行计算,然后将控制转到该 expression-statement 的结束点。如果一个 expression-statement 是可到达的,则 expression-statement 结束点也是可到达的。
1.7 选择语句
选择语句会根据表达式的值从若干个给定的语句中选择一个来执行。
selection-statement:
if-statement
switch-statement
1.7.1 if 语句
if 语句根据布尔表达式的值选择要执行的语句。
if-statement:
if ( boolean-expression )
embedded-statement
if ( boolean-expression )
embedded-statement else embedded-statement
else 部分与语法允许的、词法上最相近的上一个 if 语句相关联。因而,下列形式的 if 语句
if (x) if (y) F(); else G();
相当于
if (x) {
if (y) {
F();
}
else {
G();
}
}
if 语句按如下规则执行:
- 计算 boolean-expression(第 7.20 节)。
- 如果布尔表达式产生 true,则控制转到第一个嵌入语句。当(如果)控制到达那条语句的结束点时,控制将转到 if 语句的结束点。
- 如果布尔表达式产生 false 且如果存在 else 部分,则控制转到第二个嵌入语句。当(如果)控制到达那条语句的结束点时,控制将转到 if 语句的结束点。
- 如果布尔表达式产生 false 且如果不存在 else 部分,则控制转到 if 语句的结束点。
如果 if 语句是可到达的且布尔表达式不具有常量值 false,则 if 语句的第一个嵌入语句是可到达的。
如果 if 语句是可到达的且布尔表达式不具有常量值 true,则 if 语句的第二个嵌入语句(如果存在)是可到达的。
如果 if 语句的至少一个嵌入语句的结束点是可到达的,则 if 语句的结束点是可到达的。此外,对于不具有 else 部分的 if 语句,如果 if 语句是可到达的且布尔表达式不具有常量值 true,则该 if 语句的结束点是可到达的。
1.7.2 switch 语句
switch 语句选择一个要执行的语句列表,此列表具有一个相关联的 switch 标签,它对应于 switch 表达式的值。
switch-statement:
switch
(
expression ) switch-block
switch-block:
{
switch-sectionsopt }
switch-sections:
switch-section
switch-sections switch-section
switch-section:
switch-labels statement-list
switch-labels:
switch-label
switch-labels switch-label
switch-label:
case
constant-expression :
default
:
switch-statement 包含关键字 switch,后接带括号的表达式(称为 switch 表达式),然后是一个 switch-block。switch-block 包含零个或多个括在大括号内的 switch-section。每个 switch-section 包含一个或多个 switch-labels,后接一个 statement-list(第 8.2.1 节)。
switch 语句的主导类型 (governing
type) 由 switch 表达式确定。
- 如果 switch 表达式的类型为 sbyte、byte、short、ushort、int、uint、long、ulong、bool、char、string 或 enum-type,或者是对应于以上某种类型的可以为 null 的类型,则该类型就是 switch 语句的主导类型。
- 否则,必须有且只有一个用户定义的从 switch 表达式的类型到下列某个可能的主导类型的隐式转换(第 6.4 节):sbyte、byte、short、ushort、int、uint、long、ulong、char、string 或对应于以上某种类型的可以为 null 的类型。
- 否则,如果不存在这样的隐式转换,或者存在多个这样的隐式转换,则会发生编译时错误。
每个 case 标签的常量表达式都必须表示一个可隐式转换(第 6.1 节)为 switch 语句的主导类型的值。如果同一 switch 语句中的两个或更多个 case 标签指定同一个常量值,则会导致编译时错误。
一个 switch 语句中最多只能有一个 default 标签。
switch 语句按如下规则执行:
- 计算 switch 表达式并将其转换为主导类型。
- 如果在该 switch 语句的 case 标签中,有一个指定的常量恰好等于 switch 表达式的值,控制将转到匹配的 case 标签后的语句列表。
- 如果在该 switch 语句的 case 标签中,指定的常量都不等于 switch 表达式的值,且如果存在一个 default 标签,则控制将转到 default 标签后的语句列表。
- 如果在该 switch 语句的 case 标签中,指定的常量都不等于 switch 表达式的值,且如果不存在 default 标签,则控制将转到 switch 语句的结束点。
如果 switch 节的语句列表的结束点是可到达的,将发生编译时错误。这称为“无贯穿”规则。下面的示例
switch (i) {
case 0:
CaseZero();
break;
case 1:
CaseOne();
break;
default:
CaseOthers();
break;
}
是有效的,这是因为没有一个 switch 节的结束点是可到达的。与 C 和 C++ 不同,执行一个 switch 节的过程不能“贯穿”到下一个 switch 节,示例
switch (i) {
case 0:
CaseZero();
case 1:
CaseZeroOrOne();
default:
CaseAny();
}
会导致编译时错误。如果要在执行一个 switch 节后继续执行另一个 switch 节,则必须使用显式的 goto case 或 goto default 语句:
switch (i) {
case 0:
CaseZero();
goto case 1;
case 1:
CaseZeroOrOne();
goto default;
default:
CaseAny();
break;
}
在一个 switch-section 中允许有多个标签。下面的示例
switch (i) {
case 0:
CaseZero();
break;
case 1:
CaseOne();
break;
case 2:
default:
CaseTwo();
break;
}
是有效的。此示例不违反“无贯穿”规则,这是因为标签 case 2: 和 default: 属于同一个 switch-section。
“无贯穿”规则防止了在 C 和 C++ 中由不经意地漏掉了 break 语句而引起的一类常见 Bug。另外,由于这个规则,switch 语句的各个 switch 节可以任意重新排列而不会影响语句的行为。例如,上面 switch 语句中的各节的顺序可以在不影响语句行为的情况下反转排列:
switch (i) {
default:
CaseAny();
break;
case 1:
CaseZeroOrOne();
goto default;
case 0:
CaseZero();
goto case 1;
}
switch 节的语句列表通常以 break、goto case 或 goto default 语句结束,但是也可以使用任何其他结构,只要它能保证对应的语句列表的结束点是不可到达的。例如,由布尔表达式 true 控制的 while 语句是永远无法到达其结束点的。同样,throw 或 return 语句始终将控制转到其他地方而从不到达它的结束点。因此,下列示例是有效的:
switch (i) {
case 0:
while (true) F();
case 1:
throw new ArgumentException();
case 2:
return;
}
switch 语句的主导类型可以是 string 类型。例如:
void DoCommand(string command) {
switch (command.ToLower()) {
case “run”:
DoRun();
break;
case “save”:
DoSave();
break;
case “quit”:
DoQuit();
break;
default:
InvalidCommand(command);
break;
}
}
与字符串相等运算符(第 7.10.7 节),switch 语句区分大小写,因而只有在 switch 表达式字符串与 case 与标签常量完全匹配时才会执行给定的 switch 节。
当 switch 语句的主导类型为 string 时,允许值 null 作为 case 标签常量。
switch-block 的 statement-list 可以包含声明语句(第 8.5 节)。在 switch 块中声明的局部变量或常量的范围是该 switch 块。
在 switch 块内,表达式上下文中使用的名称的含义必须始终相同(第 7.6.2.1 节)。
如果 switch 语句是可到达的且下列条件至少有一个为真,则给定的 switch 节的语句列表是可到达的:
- switch 表达式是一个非常量值。
- switch 表达式是一个与该 switch 节中的某个 case 标签匹配的常量值。
- switch 表达式是一个不与任何 case 标签匹配的常量值,且该 switch 节包含 default 标签。
- 该 switch 节的某个 switch 标签由一个可到达的 goto case 或 goto default 语句引用。
如果下列条件中至少有一个为真,则 switch 语句的结束点是可到达的:
- switch 语句包含一个可到达的 break 语句(它用于退出 switch 语句)。
- switch 语句是可到达的,switch 表达式是非常量值,并且不存在 default 标签。
- switch 语句是可到达的,switch 表达式是不与任何 case 标签匹配的常量值,并且不存在任何 default 标签。
1.8 迭代语句
迭代语句重复执行嵌入语句。
iteration-statement:
while-statement
do-statement
for-statement
foreach-statement
1.8.1 while 语句
while 语句按不同条件执行一个嵌入语句零次或多次。
while-statement:
while
(
boolean-expression ) embedded-statement
while 语句按如下规则执行:
- 计算 boolean-expression(第 7.20 节)。
- 如果布尔表达式产生 true,控制将转到嵌入语句。当(如果)控制到达嵌入语句的结束点(可能是通过执行一个 continue 语句)时,控制将转到 while 语句的开头。
- 如果布尔表达式产生 false,控制将转到 while 语句的结束点。
在 while 语句的嵌入语句内,break 语句(第 8.9.1 节)可用于将控制转到 while 语句的结束点(从而结束嵌入语句的迭代),而 continue 语句(第 8.9.2 节)可用于将控制转到嵌入语句的结束点(从而执行 while 语句的另一次迭代)。
如果 while 语句是可到达的且布尔表达式不具有常量值 false,则 while 语句的嵌入语句是可到达的。
如果下列条件中至少有一个为真,则 while 语句的结束点是可到达的:
- while 语句包含一个可到达的 break 语句(它用于退出 while 语句)。
- while 语句是可到达的且布尔表达式不具有常量值 true。
1.8.2 do 语句
do 语句按不同条件执行一个嵌入语句一次或多次。
do-statement:
do
embedded-statement while (
boolean-expression ) ;
do 语句按如下规则执行:
- 控制转到嵌入语句。
- 当(如果)控制到达嵌入语句的结束点(可能是由于执行了一个 continue 语句)时,计算 boolean-expression(第 7.20 节)。如果布尔表达式产生 true,控制将转到 do 语句的起点。否则,控制转到 do 语句的结束点。
在 do 语句的嵌入语句内,break 语句(第 8.9.1 节)可用于将控制转到 do 语句的结束点(从而结束嵌入语句的迭代),而 continue 语句(第 8.9.2 节)可用于将控制转到嵌入语句的结束点。
如果 do 语句是可到达的,则 do 语句的嵌入语句是可到达的。
如果下列条件中至少有一个为真,则 do 语句的结束点是可到达的:
- do 语句包含一个可到达的 break 语句(它用于退出 do 语句)。
- 嵌入语句的结束点是可到达的且布尔表达式不具有常量值 true。
1.8.3 for 语句
for 语句计算一个初始化表达式序列,然后,当某个条件为真时,重复执行相关的嵌入语句并计算一个迭代表达式序列。
for-statement:
for
(
for-initializeropt ; for-conditionopt ;
for-iteratoropt ) embedded-statement
for-initializer:
local-variable-declaration
statement-expression-list
for-condition:
boolean-expression
for-iterator:
statement-expression-list
statement-expression-list:
statement-expression
statement-expression-list , statement-expression
for-initializer(如果存在)由一个 local-variable-declaration(第 8.5.1 节),或由一个用逗号分隔的 statement-expression(第 8.6 节)列表组成。用 for-initializer 声明的局部变量的范围从该变量的 local-variable-declarator 开始,一直延伸到嵌入语句的结尾。该范围包括 for-condition 和 for-iterator。
for-condition(如果存在)必须是一个 boolean-expression(第 7.20 节)。
for-iterator(如果存在)包含一个用逗号分隔的 statement-expression(第 8.6 节)列表。
for 语句按如下规则执行:
- 如果存在
for-initializer,则按变量初始值设定项或语句表达式的编写顺序执行它们。此步骤只执行一次。 - 如果存在
for-condition,则计算它。 - 如果不存在
for-condition 或如果计算产生 true,控制将转到嵌入语句。当(如果)控制到达嵌入语句的结束点(可能是因为执行了一个 continue 语句)时,则按顺序计算 for-iterator 的表达式(如果有),然后从上述步骤中的计算 for-condition 开始,执行另一次迭代。 - 如果存在
for-condition,且计算产生 false,控制将转到 for 语句的结束点。
在 for 语句的嵌入语句内,break 语句(第 8.9.1 节)可用于将控制转到 for 语句的结束点(从而结束嵌入语句的迭代),而 continue 语句(第 8.9.2 节)可用于将控制转到嵌入语句的结束点(从而执行 for-iterator 并从 for-condition 开始执行 for 语句的另一次迭代)。
如果下列条件之一为真,则 for 语句的嵌入语句是可到达的:
- for 语句是可到达的且不存在 for-condition。
- for 语句是可到达的,并且存在一个 for-condition,它不具有常量值 false。
如果下列条件中至少有一个为真,则 for 语句的结束点是可到达的:
- for 语句包含一个可到达的 break 语句(它用于退出 for 语句)。
- for 语句是可到达的,并且存在一个 for-condition,它不具有常量值 true。
1.8.4 foreach 语句
foreach 语句用于枚举一个集合的元素,并对该集合中的每个元素执行一次相关的嵌入语句。
foreach-statement:
foreach
(
local-variable-type
identifier in expression )
embedded-statement
foreach 语句的
type 和 identifier 声明该语句的迭代变量 (iteration variable)。如果以 local-variable-type 形式给定 var 标识符,并且作用域内没有名为 var 的类型,则称该迭代变量为隐式类型化迭代变量 (implicitly typed iteration
variable),并假定其类型为 foreach
语句的元素类型,如下面所指定。迭代变量相当于一个其范围覆盖整个嵌入语句的只读局部变量。在 foreach 语句执行期间,迭代变量表示当前正在为其执行迭代的集合元素。如果嵌入语句试图修改迭代变量(通过赋值或 ++ 和 ‑‑ 运算符)或将迭代变量作为 ref 或 out 参数传递,则将发生编译时错误。
为简洁起见,下面用 IEnumerable、IEnumerator、IEnumerable<T> 和 IEnumerator<T> 指代命名空间 System.Collections 和 System.Collections.Generic 中相应的类型。
foreach 语句的编译时处理首先确定表达式的集合类型 (collection type)、枚举器类型 (enumerator type) 和元素类型 (element type)。此确定过程按如下进行:
- 如果 expression 的类型 X 是数组类型,则存在从 X 到 IEnumerable 接口(因为 System.Array 实现此接口)的隐式引用转换。集合类型为 IEnumerable 接口,枚举器类型为 IEnumerator 接口,而元素类型为数组类型 X 的元素类型。
- 如果 expression 的类型 X 为 dynamic,则存在从
expression 到 IEnumerable 接口(第 6.1.8 节)的隐式转换。集合类型是 IEnumerable 接口,枚举器类型是 IEnumerator 接口。如果以 local-variable-type 形式给定 var 标识符,则元素类型 (element type) 为 dynamic,否则,它为 object。 - 否则,确定类型 X 是否具有相应的 GetEnumerator 方法:
- 在带有标识符 GetEnumerator 和不带类型参数的类型 X 上执行成员查找。如果成员查找没有产生匹配项,产生了多义性,或者产生了不是方法组的匹配项,请按如下所述检查可枚举的接口。建议在成员查找产生除方法组外的任何匹配项或没有产生匹配项的情况下发出警告。
- 使用产生的方法组和空的参数列表执行重载决策。如果重载决策产生了不适用的方法、多义性或者单个最佳方法(但该方法是静态的或非公共的),请按如下所述检查可枚举的接口。建议在重载决策产生除无歧义的公共实例方法外的任何方法或没有产生适用方法的情况下发出警告。
- 如果 GetEnumerator 方法的返回类型 E 不是类、结构或接口类型,则将产生错误,并且不再执行进一步的操作。
- 在带有标识符 Current 和不带类型参数的 E 上执行成员查找。如果成员查找没有产生匹配项,结果是错误的或者是除允许读取的公共实例属性外的任何项,则将产生错误并且不再执行进一步的操作。
- 在带有标识符 MoveNext 和不带类型参数的 E 上执行成员查找。如果成员查找没有产生匹配项,结果是错误的或者是除方法组外的任何项,则将产生错误并且不再执行进一步的操作。
- 使用空的参数列表对方法组执行重载决策。如果重载决策产生了不适用的方法、多义性、单个最佳方法(但该方法是静态的或非公共的)或者其返回类型不是 bool,则将产生错误并且不再执行进一步的操作。
- 集合类型 (collection type) 为 X,枚举器类型
(enumerator type) 为 E,而元素类型
(element type) 为 Current
属性的类型。 - 否则,检查可枚举的接口:
- 如果在所有存在从 X 到 IEnumerable<Ti> 的隐式转换的类型 Ti 中有一个唯一类型 T,以使
T 不是 dynamic,并且对于所有其他 Ti,存在从 IEnumerable<T> 到 IEnumerable<Ti> 的隐式转换,则集合类型为接口 IEnumerable<T>,枚举器类型为接口 IEnumerator<T>,而元素类型为 T。 - 否则,如果存在多个此种类型 T,则将产生错误并且不再执行进一步的操作。
- 否则,如果存在从 X 到 System.Collections.IEnumerable 接口的隐式转换,则集合类型为此接口,枚举器类型为接口 System.Collections.IEnumerator,元素类型为 object。
- 否则,将产生错误并且不再执行进一步的操作。
上述步骤如果成功,将无歧义地产生集合类型 C、枚举器类型 E 和元素类型 T。以下形式的 foreach 语句
foreach (V v in x) embedded-statement
然后扩展为:
{
E e = ((C)(x)).GetEnumerator();
try {
while (e.MoveNext()) {
V v = (V)(T)e.Current;
embedded-statement
}
}
finally {
… // Dispose e
}
}
变量 e 对表达式 x 或嵌入语句或该程序的任何其他源代码均不可见或不可访问。变量 v 在嵌入语句中是只读的。如果不存在从 T (元素类型)到 V (foreach 语句中的 V (the
local-variable-type)的显式转换(第 6.2 节),则会出错且不会执行下面的步骤。如果 x 具有值 null,则将在运行时引发 System.NullReferenceException。
只要行为与上述扩展一致,便允许通过某个实现以不同方式来实现给定的 foreach 语句(如,由于性能原因)。
v 在 while 循环中的位置对它被 embedded-statement 中出现的任何匿名函数捕获的方式十分重要。
例如:
int[] values
= { 7, 9, 13 };
Action f = null;
foreach (var
value in values)
{
if (f == null) f = () =>
Console.WriteLine(“First value: ” + value);
}
f();
如果 v 在 while 循环外声明,它将在所有迭代之间共享,其值在 for 循环结束后将成为最终值 13,该值是调用 f 时将输出的内容。然而,因为每次迭代都有自己的变量 v,在第一次迭代中由 f 捕获的该变量将继续保存值 7,该值是将输出的内容。(注意:早期版本的 C# 在 while 循环外声明 v。)
按照下列步骤构造
finally 块体:
- 如果存在从
E 到 System.IDisposable 接口的隐式转换,则 - 如果 E 为不可以为 null 值的类型,则 finally 子句扩展为下面子句的语义等效项:
finally {
((System.IDisposable)e).Dispose();
}
- 否则 finally 子句扩展到下面子句的语义等效项:
finally {
if (e != null)
((System.IDisposable)e).Dispose();
}
但如果
E 是值类型或实例化为值类型的类型形参,则从 e 到 System.IDisposable 的强制转换不会导致发生装箱。
- 否则,如果
E 是密封类型,finally
子句将扩展为一个空块:
finally {
}
- 否则,finally 子句将扩展为:
finally {
System.IDisposable d = e as System.IDisposable;
if (d != null) d.Dispose();
}
局部变量 d 对于任何用户代码均不可见或不可访问。尤其是,它不会与范围包括该 finally 块的其他任何变量发生冲突。
foreach 按如下顺序遍历数组的元素:对于一维数组,按递增的索引顺序遍历元素,从索引 0 开始,到索引 Length – 1 结束。对于多维数组,按这样的方式遍历元素:首先增加最右边维度的索引,然后是它的左边紧邻的维度,依此类推直到最左边的那个维度。
下列示例按照元素的顺序打印出一个二维数组中的各个元素的值:
using System;
class Test
{
static void Main() {
double[,] values = {
{1.2, 2.3, 3.4, 4.5},
{5.6, 6.7, 7.8, 8.9}
};
foreach (double elementValue in values)
Console.Write(“{0} “,
elementValue);
Console.WriteLine();
}
}
所生成的输出如下:
1.2 2.3 3.4
4.5 5.6 6.7 7.8 8.9
在下面的示例中
int[] numbers = { 1, 3, 5, 7, 9 };
foreach (var n in numbers) Console.WriteLine(n);
n 的类型推断为 int,即 numbers 的元素类型。
1.9 跳转语句
跳转语句用于无条件地转移控制。
jump-statement:
break-statement
continue-statement
goto-statement
return-statement
throw-statement
跳转语句将控制转到的位置称为跳转语句的目标 (target)。
当一个跳转语句出现在某个块内,而该跳转语句的目标在该块之外时,就称该跳转语句退出 (exit) 该块。虽然跳转语句可以将控制转到一个块外,但它永远不能将控制转到一个块的内部。
由于存在 try 语句的干扰,跳转语句的执行有时会变得复杂起来。如果没有这样的 try 语句,则跳转语句无条件地将控制从跳转语句转到它的目标。当跳转涉及到 try 语句时,执行就变得复杂一些了。如果跳转语句欲退出的是一个或多个具有相关联的 finally 块的 try 块,则控制最初转到最里层的 try 语句的 finally 块。当(如果)控制到达该 finally 块的结束点时,控制就转到下一个封闭 try 语句的 finally 块。此过程不断重复,直到执行完所有涉及的 try 语句的 finally 块。
在下面的示例中
using
System;
class Test
{
static void Main() {
while (true) {
try {
try {
Console.WriteLine(“Before
break”);
break;
}
finally {
Console.WriteLine(“Innermost
finally block”);
}
}
finally {
Console.WriteLine(“Outermost
finally block”);
}
}
Console.WriteLine(“After
break”);
}
}
在将控制转到跳转语句的目标之前,要先执行与两个 try 语句关联的 finally 块。
所生成的输出如下:
Before break
Innermost finally block
Outermost finally block
After break
1.9.1 break 语句
break 语句将退出直接封闭它的 switch、while、do、for 或 foreach
语句。
break-statement:
break
;
break 语句的目标是直接封闭它的 switch、while、do、for 或 foreach
语句的结束点。如果 break 语句不是由 switch、while、do、for 或 foreach 语句封闭的,则会发生编译时错误。
当多个 switch、while、do、for 或 foreach
语句互相嵌套时,break 语句将只应用于最里层的那个语句。若要穿越多个嵌套层转移控制,必须使用 goto 语句(第 8.9.3 节)。
break 语句不能退出 finally 块(第
8.10 节)。当 break 语句出现在 finally 块中时,该 break 语句的目标必须位于同一个 finally 块中,否则将发生编译时错误。
break 语句按如下规则执行:
- 如果 break 语句要退出的是一个或多个具有关联的 finally 块的 try 块,则控制最初将转到最里层的 try 语句的 finally 块。当(如果)控制到达该 finally 块的结束点时,控制就转到下一个封闭 try 语句的 finally 块。此过程不断重复,直到执行完所有涉及的 try 语句的 finally 块。
- 控制转到 break 语句的目标。
由于 break 语句无条件地将控制转到别处,因此永远无法到达 break 语句的结束点。
1.9.2 continue 语句
continue 语句将开始直接封闭它的 while、do、for 或 foreach 语句的一次新迭代。
continue-statement:
continue
;
continue 语句的目标是直接封闭它的 while、do、for 或 foreach 语句的嵌入语句的结束点。如果 continue 语句不是由 while、do、for 或 foreach
语句封闭的,则会发生编译时错误。
当多个 while、do、for 或 foreach 语句互相嵌套时,continue 语句将只应用于最里层的那个语句。若要穿越多个嵌套层转移控制,必须使用 goto 语句(第 8.9.3 节)。
continue 语句不能退出 finally 块(第
8.10 节)。当 continue
语句出现在 finally 块中时,该 continue 语句的目标必须位于同一个 finally 块中,否则将发生编译时错误。
continue 语句按如下规则执行:
- 如果 continue
语句要退出的是一个或多个具有关联的 finally 块的 try 块,则控制最初将转到最里层的 try 语句的 finally 块。当(如果)控制到达该 finally 块的结束点时,控制就转到下一个封闭 try 语句的 finally 块。此过程不断重复,直到执行完所有涉及的 try 语句的 finally
块。 - 控制转到
continue 语句的目标。
由于 continue
语句无条件地将控制转到别处,因此永远无法到达 continue 语句的结束点。
1.9.3 goto 语句
goto 语句将控制转到由标签标记的语句。
goto-statement:
goto
identifier ;
goto case
constant-expression ;
goto
default
;
goto identifier 语句的目标是具有给定标签的标记语句。如果当前函数成员中不存在具有给定名称的标签,或者如果 goto 语句不在该标签的范围内,则发生编译时错误。此规则允许使用 goto 语句将控制转移“出”嵌套范围,但是不允许将控制转移“进”嵌套范围。在下面的示例中
using System;
class Test
{
static void Main(string[] args) {
string[,] table = {
{“Red”,
“Blue”, “Green”},
{“Monday”,
“Wednesday”, “Friday”}
};
foreach (string str in args) {
int row, colm;
for (row = 0; row <= 1; ++row)
for (colm = 0; colm <= 2;
++colm)
if (str == table[row,colm])
goto done;
Console.WriteLine(“{0} not
found”, str);
continue;
done:
Console.WriteLine(“Found {0}
at [{1}][{2}]”, str, row, colm);
}
}
}
goto 语句用于将控制转移出嵌套范围。
goto case 语句的目标是直接封闭着它的 switch 语句(第 8.7.2 节)中的语句列表,该语句包含一个具有给定常量值的
case 标签。如果
goto case 语句不是由 switch 语句封闭的,或者 constant-expression 不能隐式转换(第 6.1 节)为直接封闭着它的 switch
语句的主导类型,或者直接封闭着它的 switch 语句不包含具有给定常量值的 case 标签,则发生编译时错误。
goto default 语句的目标是直接封闭着它的 switch 语句(第 8.7.2 节)中的语句列表,该语句包含一个 default
标签。如果 goto default 语句不是由 switch 语句封闭的,或者如果直接封闭着它的 switch 语句不包含 default 标签,则将发生编译时错误。
goto 语句不能退出 finally 块(第
8.10 节)。当 goto 语句出现在 finally 块中时,该 goto 语句的目标必须位于同一个 finally 块中,否则将发生编译时错误。
goto 语句按如下规则执行:
- 如果 goto 语句要退出的是一个或多个具有关联的 finally 块的 try 块,则控制最初将转到最里层的 try 语句的 finally 块。当(如果)控制到达该 finally 块的结束点时,控制就转到下一个封闭 try 语句的 finally 块。此过程不断重复,直到执行完所有涉及的 try 语句的 finally
块。 - 控制转到
goto 语句的目标。
由于 goto 语句无条件地将控制转到别处,因此永远无法到达 goto 语句的结束点。
1.9.4 return 语句
return 语句会将控制返回到出现 return 语句的函数的当前调用方。
return-statement:
return
expressionopt ;
不带表达式的 return
语句只能用在不计算值的函数成员中,即只能用在结果类型(第 10.6.10 节)为 void 的方法、属性或索引器的 set 访问器、事件的 add 和 remove 访问器、实例构造函数、静态构造函数或析构函数中。
带表达式的 return
语句只能用在计算值的函数成员中,即结果类型为非 void 的方法、属性或索引器的 get 访问器或用户定义的运算符。必须存在一个隐式转换(第 6.1 节),它能将该表达式的类型转换到包含它的函数成员的返回类型。
Return 语句也可以用在匿名函数表达式(第 7.15 节)的主体中,并参与决定这些函数存在哪些转换。
return 语句出现在
finally 块(第
8.10 节)中是编译时错误。
return 语句按如下规则执行:
- 如果 return
语句指定一个表达式,则计算该表达式,并将结果隐式转换为包含它的函数成员的返回类型。转换的结果将成为函数所生成的结果值。 - 如果 return
语句由一个或多个具有关联的 finally 块的 try 或 catch 块封闭,则控制最初将转到最里层的 try 语句的 finally 块。当(如果)控制到达该 finally 块的结束点时,控制就转到下一个封闭 try 语句的 finally
块。此过程不断重复,直到执行完所有封闭的 try 语句的 finally 块。 - 如果包含函数不是异步函数,则控制将与结果值(如果有)一起返回给包含函数的调用方。
- 如果包含函数是异步函数,则控制将返回给当前调用方,而结果值(如果有)将记录在返回任务中,如第 10.14.1 节所述。
由于 return
语句无条件地将控制转到别处,因此永远无法到达 return 语句的结束点。
1.9.5 throw 语句
throw 语句将引发一个异常。
throw-statement:
throw
expressionopt ;
带表达式的 throw 语句引发一个异常,此异常的值就是通过计算该表达式而产生的值。该表达式必须表示类类型 System.Exception 的值、从 System.Exception 派生的类类型的值,或者其有效基类从 System.Exception 派生的类型形参的值。如果表达式的计算产生 null,则将改为引发 System.NullReferenceException。
不带表达式的 throw 语句只能用在 catch 块中,在这种情况下,该语句重新引发当前正由该 catch 块处理的那个异常。
由于 throw 语句无条件地将控制转到别处,因此永远无法到达 throw 语句的结束点。
引发一个异常 E 时,控制转到封闭 try 语句中能够处理该异常的第一个 catch 子句。从引发一个异常开始直至将控制转到关于该异常的一个合适的异常处理程序止,这个过程称为异常传播 (exception propagation)。“传播一个异常”由重复地执行下列各步骤组成,直至找到一个与该异常匹配的 catch 子句。在此描述中,引发点 (throw point) 最初是指引发该异常的位置。
- 在当前函数成员中,检查每个封闭着引发点的 try 语句。对于每个语句 S(按从最里层的 try 语句开始,逐次向外,直到最外层的 try 语句结束),计算下列步骤:
- 如果 S 的 try 块封闭着引发点,并且如果 S 具有一个或多个 catch 子句,则按其出现的顺序检查这些 catch 子句以找到合适的异常处理程序。指定异常类型 T(或运行时表示异常类型 T 的类型参数)的第一个 catch 子句,以便将派生自 T 的 E 的运行时类型视为匹配项。常规 catch 子句(第 8.10 节)被认为是任何异常类型的匹配项。如果找到匹配的
catch 子句,则通过将控制转到该 catch 子句的块来完成异常传播。 - 否则,如果
S 的 try 块或 catch 块封闭着引发点并且如果 S 具有 finally 块,则控制将转到 finally 块。如果在该 finally 块内引发另一个异常,则终止当前异常的处理。否则,当控制到达 finally 块的结束点时,将继续对当前异常的处理。 - 如果在当前函数调用中没有找到异常处理程序,则将终止该函数调用并发生以下情况之一:
- 如果当前函数是非异步的,则使用与调用函数成员的语句对应的引发点,为该函数的调用方重复执行上面的步骤。
- 如果当前函数是异步的并且返回任务,则将异常记录在返回任务中,并将该任务置于“出错”或“取消”状态,如第 10.14.1 节所述。
- 如果当前函数是异步的并且返回 void,则将通知当前线程的同步上下文,如第 10.14.2 节所述。
- 如果上述异常处理终止了当前线程中的所有函数成员调用,这表明此线程没有该异常的处理程序,那么线程本身将终止。此类终止会产生什么影响,应由实现来定义。
1.10 try
语句
try 语句提供一种机制,用于捕捉在块的执行期间发生的各种异常。此外,try 语句还能让您指定一个代码块,并保证当控制离开 try 语句时,总是先执行该代码。
try-statement:
try
block catch-clauses
try
block finally-clause
try
block catch-clauses finally-clause
catch-clauses:
specific-catch-clauses
general-catch-clauseopt
specific-catch-clausesopt
general-catch-clause
specific-catch-clauses:
specific-catch-clause
specific-catch-clauses
specific-catch-clause
specific-catch-clause:
catch
(
type identifieropt )
block
general-catch-clause:
catch
block
finally-clause:
finally
block
有三种可能的 try 语句形式:
- 一个 try 块后接一个或多个 catch 块。
- 一个 try 块后接一个 finally 块。
- 一个 try 块后接一个或多个 catch 块,后面再跟一个 finally 块。
当 catch 子句指定 type 时,该类型必须为 System.Exception、从 System.Exception 派生的类型,或者其有效基类派生自 System.Exception 的类型形参类型。
当 catch 子句同时指定 class-type 和 identifier 时,相当于声明了一个具有给定名称和类型的异常变量 (exception variable)。此异常变量相当于一个范围覆盖整个 catch 块的局部变量。在 catch 块的执行期间,此异常变量表示当前正在处理的异常。出于明确赋值检查的目的,此异常变量被认为在它的整个范围内是明确赋值的。
除非 catch 子句包含一个异常变量名,否则在该 catch 块中就不可能访问当前发生的异常对象。
既不指定异常类型也不指定异常变量名的 catch 子句称为常规 catch 子句。try 语句只能有一个常规 catch 子句,而且如果存在,它必须是最后一个 catch 子句。
有些编程语言可能支持一些异常,它们不能表示为从 System.Exception 派生的对象,尽管 C# 代码可能永远不会产生这类异常。可以使用常规 catch 子句来捕捉这类异常。因此,常规的 catch 子句在语义上不同于指定了 System.Exception 类型的那些子句,因为前者还可以捕获来自其他语言的异常。
为了找到当前发生了的异常的处理程序,catch 子句是按其词法顺序进行检查的。如果 catch 子句指定的类型与同一 try 块的某个较早的 catch 子句中所指定的类型相同,或者是从该类型派生的类型,则发生编译时错误。如果没有这个限制,就可能写出不可到达的 catch 子句。
在 catch 块内,不含表达式的 throw 语句(第 8.9.5 节)可用于重新引发由该 catch 块捕捉到的异常。对异常变量的赋值不会改变上述被重新引发的异常。
在下面的示例中
using System;
class Test
{
static void F() {
try {
G();
}
catch (Exception e) {
Console.WriteLine(“Exception
in F: ” + e.Message);
e = new Exception(“F”);
throw; // re-throw
}
}
static void G() {
throw new Exception(“G”);
}
static void Main() {
try {
F();
}
catch (Exception e) {
Console.WriteLine(“Exception
in Main: ” + e.Message);
}
}
}
方法 F 捕捉到一个异常,向控制台写入一些诊断信息,更改异常变量,然后重新引发该异常。重新引发的异常是原来那个被捕获的异常,因此产生的输出为:
Exception in
F: G
Exception in Main: G
如果第一个 catch 块引发了异常 e 而不是重新引发当前的异常,产生的输出就会如下所示:
Exception in
F: G
Exception in Main: F
break、continue 或 goto 语句将控制转到 finally 块外部时将导致编译时错误。当一个 break、continue 或 goto 语句出现在
finally 块中时,该语句的目标必须在同一 finally 块内,否则会发生编译时错误。
return 语句出现在
finally 块中是编译时错误。
try 语句按如下规则执行:
- 控制转到
try 块。 - 当(如果)控制到达 try 块的结束点时:
- 如果 try 语句具有 finally 块,则将执行 finally 块。
- 控制转到
try 语句的结束点。 - 如果在
try 块执行期间有一个异常传播到 try 语句: - 按 catch 子句出现的顺序(如果有)逐个对其进行检查,以找到一个合适的异常处理程序。第一个指定了异常类型或该异常类型的基类型的 catch 子句被认为是一个匹配项。常规 catch 子句被认为是任何异常类型的匹配项。如果找到匹配的 catch 子句:
- 如果匹配的
catch 子句声明一个异常变量,则异常对象被赋给该异常变量。 - 控制转到匹配 catch 块。
- 当(如果)控制到达 catch 块的结束点时:
- 如果 try 语句具有 finally 块,则将执行 finally 块。
- 控制转到
try 语句的结束点。- 如果在
try 块执行期间有一个异常传播到 catch 语句: - 如果 try 语句具有 finally 块,则将执行 finally 块。
- 该异常就传播到更外面一层(封闭)的 try 语句。
- 如果 try 语句没有 catch 子句或者如果没有与异常匹配的 catch 子句:
- 如果 try 语句具有 finally 块,则将执行 finally 块。
- 该异常就传播到更外面一层(封闭)的 try 语句。
- 如果在
- 如果匹配的
finally 块中的语句始终在控制离开 try 语句时执行。无论是什么原因引起控制转移(正常执行到达结束点,执行了 break、continue、goto 或 return 语句,或是将异常传播到 try 语句之外),情况都是如此。
如果在执行 finally
块期间引发了一个异常,而且该异常不是在同一个 finally 块中捕获的,则该异常将被传播到下一个封闭的 try 语句。与此同时,原先那个正在传播过程中的异常(如果存在)就会被丢弃。关于传播异常的过程,在 throw 语句(第 8.9.5 节)的说明中有进一步讨论。
如果 try 语句是可到达的,则 try 语句的 try 块也是可到达的。
如果 try 语句是可到达的,则 try 语句的 catch 块也是可到达的。
如果 try 语句是可到达的,则 try 语句的 finally 块也是可到达的。
如果下列两个条件都为真,则 try 语句的结束点是可到达的:
- try 块的结束点是可到达的或者至少一个 catch 块的结束点是可到达的。
- 如果存在一个 finally 块,则此
finally 块的结束点是可到达的。
1.11 checked 语句和 unchecked 语句
checked 语句和
unchecked 语句用于控制整型算术运算和转换的溢出检查上下文。
checked-statement:
checked
block
unchecked-statement:
unchecked
block
checked 语句使
block 中的所有表达式都在一个选中的上下文中进行计算,而 unchecked 语句使 block 中的所有表达式都在一个未选中的上下文中进行计算。
checked 语句和
unchecked 语句完全等效于 checked 和 unchecked 运算符(第 7.6.12 节),不同的是它们作用于块,而不是作用于表达式。
1.12 lock 语句
lock 语句用于获取某个给定对象的互斥锁,执行一个语句,然后释放该锁。
lock-statement:
lock
(
expression ) embedded-statement
lock 语句的表达式必须表示一个已知的 reference-type 类型的值。永远不会为 lock 语句中的表达式执行隐式装箱转换(第 6.1.7 节),因此,如果该表达式表示的是一个 value-type 的值,则会导致一个编译时错误。
下列形式的 lock 语句
lock (x) …
(其中 x 是一个 reference-type 的表达式)完全等效于
bool
__lockWasTaken = false;
try {
System.Threading.Monitor.Enter(x, ref
__lockWasTaken);
…
}
finally {
if (__lockWasTaken)
System.Threading.Monitor.Exit(x);
}
不同的只是:实际执行中 x 只计算一次。
当一个互斥锁已被占用时,在同一线程中执行的代码仍可以获取和释放该锁。但是,在其他线程中执行的代码在该锁被释放前是无法获得它的。
建议不要使用锁定 System.Type 对象的方法来同步对静态数据的访问。其他代码可能会在同一类型上进行锁定,这会导致死锁。更好的方法是通过锁定私有静态对象来同步对静态数据的访问。例如:
class Cache
{
private static readonly object
synchronizationObject = new object();
public static void Add(object x) {
lock (Cache.synchronizationObject) {
…
}
}
public static void Remove(object x) {
lock (Cache.synchronizationObject) {
…
}
}
}
1.13 using 语句
using 语句获取一个或多个资源,执行一个语句,然后释放该资源。
using-statement:
using
(
resource-acquisition ) embedded-statement
resource-acquisition:
local-variable-declaration
expression
一个资源 (resource) 是实现了
System.IDisposable 的类或结构,它只包含一个名为 Dispose 的不带形参的方法。正在使用资源的代码可以调用 Dispose 以表明不再需要该资源。如果不调用 Dispose,则最终将因为垃圾回收而对该资源进行自动释放。
如果 resource-acquisition 的形式是 local-variable-declaration,则 local-variable-declaration 的类型必须为 dynamic 或是可以隐式转换为 System.IDisposable 的类型。如果 resource-acquisition 的形式是 expression,则表达式必须可以隐式转换为 System.IDisposable。
在 resource-acquisition 中声明的局部变量是只读的,且必须包含一个初始值设定项。如果嵌入语句试图修改这些局部变量(通过赋值或 ++ 和 ‑‑ 运算符),获取它们的地址或将它们作为 ref 或 out 形参传递,则将发生编译时错误。
using 语句转换为三部分:获取、使用和释放。资源的使用部分被隐式封闭在一个含有 finally 子句的 try 语句中。此 finally
子句用于释放资源。如果所获取资源是 null,则不会对 Dispose 进行调用,也不会引发任何异常。如果资源类型为 dynamic,则会在获取期间通过隐式动态转换(第 6.1.8 节)的方式动态转换为 IDisposable,以确保在使用和释放之前转换成功。
下列形式的 using 语句
using
(ResourceType resource = expression) statement
对应于下列三个可能的扩展中的一个。当 ResourceType 是不可以为 null 的值类型,扩展为
{
ResourceType resource = expression;
try {
statement;
}
finally {
((IDisposable)resource).Dispose();
}
}
否则,当 ResourceType 是 dynamic
之外可以为 null 的值类型时,扩展为
{
ResourceType resource = expression;
try {
statement;
}
finally {
IDisposable d =
(IDisposable)resource;
if (resource != null) d.Dispose();
}
}
否则,当 ResourceType 为 dynamic
时,扩展为
{
ResourceType resource = expression;
IDisposable d = resource;
try {
statement;
}
finally {
if (d != null) d.Dispose();
}
}
在上面任何一种扩展中,resource 变量在嵌入语句中都是只读的,d 变量在嵌入语句中既不可访问,也不可见。
由于性能或其他方面原因,只要该行为与上面的扩展一致,就可以以不同方式实现给定 using 语句。
下列形式的 using 语句
using
(expression) statement
具有三种相同的可用扩展。在这种情况下,ResourceType 隐含地为 expression 编译时类型(如果有该类型)。否则,使用接口 IDisposable 自身作为 ResourceType。在嵌入语句中不可访问 resource 变量,并且该变量对嵌入语句不可见。
如果 resource-acquisition 采用 local-variable-declaration 的形式,则有可能获取给定类型的多个资源。下列形式的 using 语句
using
(ResourceType r1 = e1, r2 = e2, …, rN = eN) statement
完全等效于嵌套 using 语句的序列:
using
(ResourceType r1 = e1)
using (ResourceType r2 = e2)
…
using
(ResourceType rN = eN)
statement
下面的示例创建一个名为 log.txt
的文件并将两行文本写入该文件。然后该示例打开这个文件进行读取,并将它所包含的文本行复制到控制台。
using System;
using System.IO;
class Test
{
static void Main() {
using (TextWriter w =
File.CreateText(“log.txt”)) {
w.WriteLine(“This is line
one”);
w.WriteLine(“This is line
two”);
}
using (TextReader r =
File.OpenText(“log.txt”)) {
string s;
while ((s = r.ReadLine()) != null)
{
Console.WriteLine(s);
}
}
}
}
由于 TextWriter 和 TextReader 类实现了 IDisposable 接口,因此该示例可以使用 using 语句以确保所涉及的文件在写入或读取操作后正确关闭。
1.14 yield 语句
yield 语句用在迭代器块中(第 8.2 节),作用是向迭代器的枚举器对象(第 10.14.4 节)或可枚举对象(第 10.14.5 节)产生一个值,或者通知迭代结束。
yield-statement:
yield
return
expression ;
yield
break
;
yield 不是保留字;它仅在紧靠 return 或 break 关键字之前使用时才具有特殊意义。在其他上下文中,yield 可用作标识符。
yield 语句可出现的位置存在几个限制,如下所述。
- 如果 yield 语句(包括两种形式)出现在 method-body、operator-body 或 accessor-body 之外,则会引起编译时错误。
- 如果 yield 语句(包括两种形式)出现在匿名函数内部,则会引起编译时错误。
- yield 语句(两种形式中的任一形式)出现在 try 语句的 finally 子句中时,会导致编译时错误。
- yield return 语句出现在包含任何 catch 子句的 try 语句内的任何位置时,会导致编译时错误。
下面的示例演示 yield 语句的有效用法和无效用法。
delegate
IEnumerable<int> D();
IEnumerator<int>
GetEnumerator() {
try {
yield return 1; // Ok
yield break; // Ok
}
finally {
yield return 2; // Error, yield in finally
yield break; // Error, yield in finally
}
try {
yield return 3; // Error, yield return in try…catch
yield break; // Ok
}
catch {
yield return 4; // Error, yield return in try…catch
yield break; // Ok
}
D d = delegate {
yield return 5; // Error, yield in an anonymous function
};
}
int
MyMethod() {
yield return 1; // Error, wrong return type for an iterator block
}
yield return 语句中的表达式的类型必须能够隐式转换(第 6.1 节)为迭代器的产生类型(第 10.14.3 节)。
yield return 语句按如下规则执行:
- 计算该语句中给出的表达式,隐式转换为产生类型,并赋给枚举器对象的 Current 属性。
- 迭代器块的执行被挂起。如果 yield return 语句在一个或多个 try 块内,则此时与之关联的 finally 块将不会执行。
- 枚举器对象的 MoveNext 方法向其调用方返回 true,指示枚举器对象成功前进到下一项。
下次调用枚举器对象的 MoveNext
方法时将从上次挂起的地方恢复迭代器块的执行。
yield break 语句按如下规则执行:
- 如果 yield break 语句由一个或多个具有关联的 finally 块的 try 块封闭,则控制最初将转到最里层的 try 语句的 finally 块。当(如果)控制到达该 finally 块的结束点时,控制就转到下一个封闭 try 语句的 finally 块。此过程不断重复,直到执行完所有封闭的 try 语句的 finally
块。 - 控制返回给迭代器块的调用方。这是枚举器对象的 MoveNext 方法或 Dispose 方法。
由于 yield break 语句无条件地将控制转到别处,因此永远无法到达 yield break 语句的结束点。