13 种语句

13.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
    | unsafe_statement   // unsafe code support
    | fixed_statement    // unsafe code support
    ;

unsafe_statement(§23.2)和fixed_statement§23.7)仅在不安全的代码(§23)中可用。

embedded_statement 非终结符用于在其它语句中出现的语句。 使用 embedded_statement 而不是 statement 会导致在这些上下文中无法使用声明语句和标记语句。

示例:代码

void F(bool b)
{
   if (b)
      int i = 44;
}

导致编译时错误,因为 if 语句的 if 分支需要 embedded_statement 而不是 statement。 如果允许此代码,则会声明变量,但永远不能使用该变量 i 。 但是,请注意,通过将 i 的声明放置在一个块中,该示例是有效的。

结束示例

13.2 终结点和可访问性

每个语句都有一个 终点。 在直观的术语中,语句的终点是紧跟语句的位置。 复合语句(包含嵌入语句的语句)的执行规则指定在控件到达嵌入语句的终点时执行的操作。

示例:当控件到达块中的语句的终点时,控件将传输到块中的下一个语句。 示例结束

如果可以通过执行来达到语句,则表示该语句是可访问的。 相反,如果不可能执行语句,则声明据说是 无法访问的。

示例:在以下代码中

void F()
{
    Console.WriteLine("reachable");
    goto Label;
    Console.WriteLine("unreachable");
  Label:
    Console.WriteLine("reachable");
}

Console.WriteLine 的第二次调用是无法访问的,因为不可能执行该语句。

示例结束

如果 throw_statementblockempty_statement 以外的语句无法访问,则会报告警告。 具体而言,语句无法访问并非错误。

注意:若要确定特定语句或终结点是可访问的,编译器将根据为每个语句定义的可访问性规则执行流分析。 流分析将考虑控制语句行为的常量表达式(§12.23)的值,但不考虑非常量表达式的可能值。 换句话说,出于控制流分析的目的,给定类型的非常量表达式被视为具有该类型的任何可能值。

在示例中

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 被认为是可访问的,尽管实际上,它永远不会执行。

尾注

函数 成员或匿名函数的块 始终被视为可访问。 通过连续评估块中每个语句的可访问性规则,可以确定任何给定语句的可访问性。

示例:在以下代码中

void F(int x)
{
    Console.WriteLine("start");
    if (x < 0)
        Console.WriteLine("negative");
}

第二个 Console.WriteLine 的可访问性确定如下:

  • 第一个 Console.WriteLine 表达式语句是可访问的,因为 F 方法的块是可访问的 (§13.3)。
  • 第一个 Console.WriteLine 表达式语句的终点是可访问的,因为该语句是可访问的(§13.7§13.3)。
  • if 语句是可访问的,因为第一个 Console.WriteLine 表达式语句的终点是可访问的(§13.7§13.3)。
  • 第二 Console.WriteLine 个表达式语句是可访问的,因为语句的 if 布尔表达式没有常量值 false

示例结束

在两种情况下,如果语句的终点无法访问,就会造成编译时错误:

  • 由于 switch 语句不允许一个 switch 部分“贯穿”到下一个 switch 部分,因此如果一个 switch 部分的语句列表的终点是可访问的,则属于编译时错误。 如果发生此错误,通常表明 break 语句缺失。

  • 如果函数成员或计算值的匿名函数块的终点是可访问的,则属于编译时错误。 如果发生此错误,则通常指示 return 缺少语句(§13.10.5)。

13.3 块

13.3.1 概述

在通常只允许编写一个语句的场合,代码块允许编写多个语句。

block
    : '{' statement_list? '}'
    ;

一个 block 包含一个可选的 statement_list (§13.3.2),并以大括号括起来。 如果省略语句列表,则表示该块为空。

块可能包含声明语句(§13.6)。 在块中声明的本地变量或常量的范围就是块。

一个块的执行过程如下:

  • 如果块为空,则控制将传输到块的终点。
  • 如果块不为空,则控制权将传输到语句列表。 当控制到达语句列表的终点时,控制权将被转移到块的终点。

如果一个块本身是可访问的,那么该块的语句列表也是可访问的。

如果块为空,或者如果语句列表的终点是可访问的,那么块的终点就是可访问的。

包含一个或多个 yield 语句 (§13.15) 的 block 被称为迭代器块。 迭代器块用于将函数成员实现为迭代器(§15.14)。 一些其他限制适用于迭代器块:

  • 在迭代器块中出现 return 语句是编译时错误(但允许 yield return 语句)。
  • 如果迭代器块包含不安全的上下文环境(§23.2),就是编译时错误。 迭代器块始终定义安全上下文,即使其声明嵌套在不安全的上下文中也是如此。

13.3.2 语句列表

语句列表由一个或多个按顺序编写的语句组成。 语句列表会出现在block (§13.3) 和 switch_block (§13.8.3) 中。

statement_list
    : statement+
    ;

语句列表是通过将控件传输到第一个语句来执行的。 当控件到达语句的终点时,控件将传输到下一个语句。 当控件到达最后一个语句的终点时,控件将传输到语句列表的终点。

如果以下条件至少有一个为 true,则语句列表中的语句是可访问的:

  • 该语句是第一个语句,语句列表本身是可访问的。
  • 上述语句的终点是可访问的。
  • 该语句是一个带标签的语句,该标签被一个可达的goto语句引用。

如果语句列表中最后一个语句的终点是可访问的,那么整个语句列表的终点也是可访问的。

13.4 空语句

empty_statement不执行任何操作。

empty_statement
    : ';'
    ;

当在需要语句的上下文中没有要执行的操作时,将使用空语句。

执行空语句只会将控制权传输到语句的终点。 因此,如果空语句是可访问的,那么空语句的终点就是可访问的。

示例:使用 null 正文编写 while 语句时,可以使用空语句:

bool ProcessMessage() {...}
void ProcessMessages()
{
    while (ProcessMessage())
        ;
}

此外,空语句可用于在块的结束“”}之前声明标签:

void F(bool done)
{
    ...
    if (done)
    {
        goto exit;
    }
    ...
  exit:
    ;
}

结束 示例

13.5 标记语句

labeled_statement允许以标签为前缀的语句。 允许在块中使用标记语句,但不允许使用嵌入语句。

labeled_statement
    : identifier ':' statement
    ;

带标签的语句使用标识符声明带有给定名称的标签。 标签的范围是声明标签的整个块,包括任何嵌套块。 对于同名的两个标签来说,如果它们的范围重叠,这就是编译时错误。

可以从标签范围内的 goto 语句(§13.10.4)引用标签。

注意:这意味着 goto 语句可以在块和块外传输控制,但永远不会传输到块中。 尾注

标签具有自己的声明空间,不会干扰其他标识符。

示例:示例

int F(int x)
{
    if (x >= 0)
    {
        goto x;
    }
    x = -x;
  x:
    return x;
}

有效,使用名称 x 作为参数和标签。

结束示例

标记语句的执行与标签后面的语句的执行完全对应。

除了正常控制流提供的可访问性外,如果一个标签是由可访问的goto语句引用的,那么该标记的语句是可访问的,除非goto语句位于try块或一个包含终结点不可访问的catch块的try_statement内部,并且标记的语句位于该try_statement外部。

13.6 声明语句

13.6.1 常规

declaration_statement声明一个或多个局部变量、一个或多个局部常量或局部函数。 声明语句可以在代码块和 switch 语句块中使用,但不允许作为嵌入式语句。

declaration_statement
    : local_variable_declaration ';'
    | local_constant_declaration ';'
    | local_function_declaration
    ;

局部变量是使用 local_variable_declaration§13.6.2)声明的。 使用local_constant_declaration§13.6.3)声明本地常量。 使用 local_function_declaration§13.6.4)声明本地函数。

声明的名称会被引入最近的封闭声明空间中 (§7.3)。

13.6.2 局部变量声明

13.6.2.1 概述

local_variable_declaration声明一个或多个局部变量。

local_variable_declaration
    : implicitly_typed_local_variable_declaration
    | explicitly_typed_local_variable_declaration
    | explicitly_typed_ref_local_variable_declaration
    ;

隐式类型声明包含上下文关键字(§6.4.4var,导致在三个类别之间出现语法歧义,其解决方式如下:

  • 如果范围内没有名为 var 的类型,而输入又与 implicitly_typed_local_variable_declaration 相匹配,那么它就会被选中;
  • 否则,如果命名 var 的类型在作用域中,则 implicitly_typed_local_variable_declaration 不被视为可能的匹配项。

local_variable_declaration 中,每个变量都由 declarator 引入,它是 implicitly_typed_local_variable_declaratorexplicitly_typed_local_variable_declaratorref_local_variable_declarator 之一,分别用于隐式类型、显式类型和引用本地变量。 声明符定义引入变量的名称(标识符)和初始值(如果有)。

如果声明中有多个声明符,则会处理这些声明符,包括任何初始化表达式(从左到右(§9.4.4.5)。

注意:对于不作为 for_initializer (§13.9.4) 或 resource_acquisition (§13.14) 出现的 local_variable_declaration 而言,这种从左到右的顺序等同于每个声明符都位于单独的 local_variable_declaration 中。 例如:

void F()
{
    int x = 1, y, z = x * 2;
}

等效于:

void F()
{
    int x = 1;
    int y;
    int z = x * 2;
}

尾注

局部变量的值是在表达式中使用 simple_name§12.8.4) 获取的。 在获取其值的每个位置,应明确分配局部变量(§9.4)。 local_variable_declaration引入的每个局部变量最初未赋值§9.4.3)。 如果声明符具有初始化表达式,则引入的局部变量被分类为 在声明符末尾分配§9.4.4.4.5)。

一个局部变量由local_variable_declaration引入,其范围定义如下(§7.7):

  • 如果声明发生在for_initializer中,则其作用域包括for_initializerfor_conditionfor_iteratorembedded_statement§13.9.4)。
  • 如果声明以 resource_acquisition 的形式出现,那么范围就是 using_statement (§13.14) 语义等效扩展的最封闭块;
  • 否则,范围就是发生声明的块。

在声明符前面的文本位置或在其声明符内的任何初始化表达式中,按名称引用局部变量是错误的。 在本地变量的作用域内,声明另一个具有相同名称的局部变量、局部函数或常量是编译时错误。

引用本地变量的 ref-safe-context (§9.7.2) 是其初始化 variable_reference 的 ref-safe-context。 非引用本地变量的 ref-safe-context 是 declaration-block

13.6.2.2 隐式类型的本地变量声明

implicitly_typed_local_variable_declaration
    : 'var' implicitly_typed_local_variable_declarator
    | ref_kind 'var' ref_local_variable_declarator
    ;

implicitly_typed_local_variable_declarator
    : identifier '=' expression
    ;

一个 implicitly_typed_local_variable_declaration 引入了一个本地变量 identifier。 表达式variable_reference应具有编译时类型T。 第一个替代项声明一个带有表达式初始值的变量;当T是一个不可为空的引用类型时,其类型是T?,否则其类型是T。 第二种替代方法声明一个初始值为refvariable_reference的ref变量;当这是一个非 null 引用类型时,其类型为ref T?,否则其类型为Tref Tref_kind§15.6.1 中有介绍。

示例:

var i = 5;
var s = "Hello";
var d = 1.0;
var numbers = new int[] {1, 2, 3};
var orders = new Dictionary<int,Order>();
ref var j = ref i;
ref readonly var k = ref i;

上述隐式类型局部变量声明与以下显式类型声明完全等效:

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>();
ref int j = ref i;
ref readonly int k = ref i;

以下是不正确的隐式类型本地变量声明:

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 v itself

结束示例

13.6.2.3 显式键入的局部变量声明

explicitly_typed_local_variable_declaration
    : type explicitly_typed_local_variable_declarators
    ;

explicitly_typed_local_variable_declarators
    : explicitly_typed_local_variable_declarator
      (',' explicitly_typed_local_variable_declarator)*
    ;

explicitly_typed_local_variable_declarator
    : identifier ('=' local_variable_initializer)?
    ;

local_variable_initializer
    : expression
    | array_initializer
    ;

explicity_typed_local_variable_declaration引入了一个或多个具有指定类型的局部变量。

如果存在local_variable_initializer,则其类型应根据简单赋值(§12.21.2)或数组初始化规则§17.7)适当,并且其值被分配为变量的初始值。

13.6.2.4 显式键入的 ref 局部变量声明

explicitly_typed_ref_local_variable_declaration
    : ref_kind type ref_local_variable_declarators
    ;

ref_local_variable_declarators
    : ref_local_variable_declarator (',' ref_local_variable_declarator)*
    ;

ref_local_variable_declarator
    : identifier '=' 'ref' variable_reference
    ;

初始化变量variable_reference的类型应为type,并满足与ref赋值§12.21.3)相同的要求。

如果ref_kindref readonly,则声明的标识符是对被视为只读的变量的引用。 否则,如果 ref_kindref,则声明的 identifier 是对应可写入的变量的引用。

在使用 method_modifierasync 声明的方法中或在迭代器 (§15.14) 中声明引用本地变量或 ref struct 类型的变量是一个编译时错误。

13.6.3 本地常量声明

local_constant_declaration声明一个或多个本地常量。

local_constant_declaration
    : 'const' type constant_declarators
    ;

constant_declarators
    : constant_declarator (',' constant_declarator)*
    ;

constant_declarator
    : identifier '=' constant_expression
    ;

local_constant_declaration的类型指定声明引入的常量的类型。 该类型后面是一个 constant_declarator 列表,而每个列表都会引入一个新常量。 constant_declarator 包含一个 identifier,用来命名常量,后面跟着一个 “=” 标记,然后是一个 constant_expression§12.23)来给出常量的值。

局部常量声明的类型constant_expression应遵循与常量成员声明(§15.4)相同的规则。

在表达式中使用 simple_name§12.8.4)获取局部常量的值。

本地常量的范围是声明所在的块。 在 constant_declarator 结束之前的文本位置引用本地变量是错误的。

声明多个常量的本地常量声明等效于具有相同类型的单个常量的多个声明。

13.6.4 本地函数声明

local_function_declaration声明本地函数。

local_function_declaration
    : local_function_modifier* return_type local_function_header
      local_function_body
    | ref_local_function_modifier* ref_kind ref_return_type
      local_function_header ref_local_function_body
    ;

local_function_header
    : identifier '(' parameter_list? ')'
    | identifier type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause*
    ;

local_function_modifier
    : ref_local_function_modifier
    | 'async'
    ;

ref_local_function_modifier
    : 'static'
    | unsafe_modifier   // unsafe code support
    ;

local_function_body
    : block
    | '=>' null_conditional_invocation_expression ';'
    | '=>' expression ';'
    ;

ref_local_function_body
    : block
    | '=>' 'ref' variable_reference ';'
    ;

语法说明:识别local_function_body时,如果null_conditional_invocation_expression表达式替代项都适用,则应选择前者。 (§15.6.1

示例:本地函数有两个常见用例:迭代器方法和异步方法。 在迭代器方法中,只有在调用枚举返回的序列的代码时才会观察到任何异常。 在异步方法中,只有在等待返回的任务时,才会观察到任何异常。 以下示例演示如何使用本地函数将参数验证与迭代器实现分离:

public static IEnumerable<char> AlphabetSubset(char start, char end)
{
    if (start < 'a' || start > 'z')
    {
        throw new ArgumentOutOfRangeException(paramName: nameof(start),
            message: "start must be a letter");
    }
    if (end < 'a' || end > 'z')
    {
        throw new ArgumentOutOfRangeException(paramName: nameof(end),
            message: "end must be a letter");
    }
    if (end <= start)
    {
        throw new ArgumentException(
            $"{nameof(end)} must be greater than {nameof(start)}");
    }
    return AlphabetSubsetImplementation();

    IEnumerable<char> AlphabetSubsetImplementation()
    {
        for (var c = start; c < end; c++)
        {
            yield return c;
        }
    }
}

结束示例

除非另有指定,否则所有语法元素的语义与 method_declaration§15.6.1)的语义相同,只是在本地函数的上下文中进行解释,而不是在方法中。

local_function_declaration的标识符在其声明的块范围中应是唯一的,包括任何封闭的局部变量声明空间。 这样做的后果之一是不允许重载 local_function_declaration

local_function_declaration可能包括一个 async§15.15) 修饰符和一个 unsafe§23.1) 修饰符。 如果声明包含 async 修饰符,则返回类型应 void 为或类型 «TaskType»§15.15.1)。 如果声明包含static修饰符,则函数是静态本地函数;否则为非静态本地函数。 对于 type_parameter_listparameter_list 而言,包含 attributes 是一个编译时错误。 如果在不安全的上下文(§23.2)中声明了本地函数,则即使本地函数声明不包含 unsafe 修饰符,本地函数也可能包含不安全的代码。

本地函数在块范围内声明。 非静态本地函数可以从封闭作用域捕获变量,而静态本地函数则不应捕获变量(因此它无法访问封闭本地、参数、非静态本地函数或 this)。 如果捕获的变量由非静态本地函数的主体读取,但在每次调用函数之前未明确分配,则这是编译时错误。 编译器应确定在返回时明确分配哪些变量(§9.4.4.33)。

this 的类型是结构类型时,本地函数的主体访问 this 会导致编译时错误。 无论访问是显式的(如 this.x)还是隐式的(如 x 中的 x 是结构的实例成员),情况都是如此。 此规则仅禁止此类访问,并不影响成员查找是否会产生结构成员的结果。

对于局部函数的主体来说,如果包含目标在局部函数主体之外的 goto 语句、break 语句或 continue 语句,则会发生编译时错误。

注意:上述thisgoto的规则与匿名函数在§12.19.3中的规则相同。 尾注

可以在声明之前从词法点调用局部函数。 但是,如果在本地函数 (§7.7) 中使用的变量的声明之前按词法声明函数,则属于编译时错误。

对于本地函数而言,如果声明的参数、类型参数或局部变量的名称与任何封闭局部变量声明空间中声明的名称相同,则会导致编译时错误。

本地函数主体始终是可访问的。 如果可访问本地函数声明的起点,则可访问本地函数声明的终结点。

示例:在以下示例中,L 的正文是可以访问的,即使 L 的起始点不可访问。 由于无法访问起点 L ,因此无法访问终结点 L 后面的语句:

class C
{
    int M()
    {
        L();
        return 1;

        // Beginning of L is not reachable
        int L()
        {
            // The body of L is reachable
            return 2;
        }
        // Not reachable, because beginning point of L is not reachable
        return 3;
    }
}

换句话说,本地函数声明的位置不会影响包含函数中任何语句的可访问性。 结束示例

如果局部函数的参数类型为 dynamic,则调用的函数应在编译时解析,而不是运行时。

不应在表达式树中使用本地函数。

静态本地函数

  • 可以从封闭范围引用静态成员、类型参数、常量定义和静态本地函数。
  • 不应引用thisbasethis,也不应从封闭范围内引用实例成员、局部变量、参数或非静态本地函数。 但是,表达式中 nameof() 允许所有这些内容。

13.7 表达式语句

expression_statement计算给定表达式。 表达式计算的值(如果有)将被丢弃。

expression_statement
    : statement_expression ';'
    ;

statement_expression
    : null_conditional_invocation_expression
    | invocation_expression
    | object_creation_expression
    | assignment
    | post_increment_expression
    | post_decrement_expression
    | pre_increment_expression
    | pre_decrement_expression
    | await_expression
    ;

并非所有表达式都允许作为语句。

注意:具体而言,仅计算一个值(将被丢弃)的表达式x + yx == 1不允许作为语句。 尾注

执行expression_statement会评估所包含的表达式,然后将控制转移到expression_statement的终点。 如果 expression_statement是可访问的,则 expression_statement 的终点也是可访问的。

13.8 选择语句

13.8.1 常规

选择语句根据某些表达式的值选择多个可能的语句之一来执行。

selection_statement
    : if_statement
    | switch_statement
    ;

13.8.2 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§12.24)进行评估。
  • 如果布尔表达式生成 true,控件将传输到第一个嵌入语句。 当控件到达该语句的终点时,控件将传输到语句的 if 终点。
  • 如果布尔表达式生成 false 并且某个 else 部分存在,则控件将传输到第二个嵌入语句。 当控件到达该语句的终点时,控件将传输到语句的 if 终点。
  • 如果布尔表达式生成 false 且部件 else 不存在,则控件将传输到语句的 if 终点。

如果if语句可访问且布尔表达式没有常量值if,则语句的第一个false嵌入语句是可访问的。

如果if语句是可访问的,并且布尔表达式没有常量值true,那么if语句的第二个嵌入语句(如果存在)是可访问的。

一个语句的if终点是可访问的,如果其至少一个嵌入语句的终点是可访问的。 此外,如果if语句可访问且布尔表达式没有常量值else,则没有if部分的语句的终点true是可访问的。

13.8.3 switch 语句

switch 语句选择执行一个语句列表,该语句列表有一个相关的 switch 标签,与 switch 表达式的值相对应。

switch_statement
    : 'switch' '(' expression ')' switch_block
    ;

switch_block
    : '{' switch_section* '}'
    ;

switch_section
    : switch_label+ statement_list
    ;

switch_label
    : 'case' pattern case_guard?  ':'
    | 'default' ':'
    ;

case_guard
    : 'when' expression
    ;

switch_statement由关键字switch组成,后跟带括号表达式(称为 switch 表达式),后跟switch_blockswitch_block 由零个或多个 switch_section 组成,并用大括号括起来。 每个 switch_section 都由一个或多个 switch_label 组成,后面跟一个 statement_list (§13.3.2)。 每个包含 caseswitch_label 都有一个相关模式 (§11),switch 表达式的值将根据该模式进行测试。 如果存在 case_guard,其表达式应可隐式转换为 bool 类型,并且该表达式将作为一个附加条件进行求值,以便认为情况已得到满足。

switch 语句的控制类型由 switch 表达式确定。

  • 如果 switch 表达式的类型为 sbytebyteshortushortintuintlongulongcharboolstring,或是 enum_type,或者它是对应于其中一种类型的可为 null 值类型,那么该类型就是 switch 语句的控制类型。
  • 否则,如果恰好存在一个用户定义的隐式转换,即从 switch 表达式的类型转换为下列可能的治理类型之一:sbytebyteshortushortintuintlongulong、、、charstring与其中一种类型对应的可为 null 的值类型,则转换的类型是语句的switch调控类型。
  • 否则,switch 语句的主导类型是 switch 表达式的类型。 如果不存在此类类型,则为错误。

语句中最多可以有一个defaultswitch标签。

如果任何开关标签的模式不适用于输入表达式的类型(§11.2.1),则这是一个错误。

如果任意开关标签的模式被(§11.3)之前开关标签的模式集合中包含,而这些标签没有条件守护或其条件守护是值为 true 的常量表达式,那么这是一个错误。

示例:

switch (shape)
{
    case var x:
        break;
    case var _: // error: pattern subsumed, as previous case always matches
        break;
    default:
        break;  // warning: unreachable, all possible values already handled.
}

结束示例

switch 语句的执行过程如下:

  • 对switch表达式进行求值并将其转换为控制类型。
  • 根据转换的开关表达式的值传输控制:
    • 在同一 switch 语句的 case 标签集合中,与 switch 表达式值相匹配的词法上的第一个模式,如果保护表达式要么不存在,要么求值为 true,就会将控制权转移到相匹配的 case 标签之后的语句列表。
    • 否则,如果存在 default 标签,控件将传输到标签后面的 default 语句列表。
    • 否则,控制权将转移到 switch 语句的终点。

注意:未定义在运行时匹配模式的顺序。 允许编译器(但不需要)按顺序匹配模式,并重复使用已匹配模式的结果来计算其他模式匹配的结果。 然而,编译器需要确定与表达式匹配的词法上第一个模式,对于该模式,临界子句要么不存在,要么计算结果为 true尾注

如果 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 casegoto 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 语句而出现的一类常见错误。 例如,可以反转上述语句的各个 switch 部分,而不会影响语句的行为:

switch (i)
{
    default:
        CaseAny();
        break;
    case 1:
        CaseZeroOrOne();
        goto default;
    case 0:
        CaseZero();
        goto case 1;
}

尾注

注意:switch 区段的语句列表通常以breakgoto casegoto default语句结尾,但是,允许任何导致语句列表终点不可到达的构造存在。 例如,while 是一个已知由布尔表达式 true 控制的语句,无法达到其终点。 同样,throwreturn语句始终会将控制权转移到其他位置,并且永远不会到达其终点。 因此,以下示例有效:

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;
    }
}

结束示例

注意:与字符串相等运算符(§12.12.8)一样,该 switch 语句区分大小写,并且仅在 switch 表达式字符串与标签常量完全匹配 case 时才执行给定的 switch 节。 end noteswitch语句的控制类型是string或可为null的值类型时,值null允许作为case标签常量。

switch_blockstatement_list可能包含声明语句(§13.6)。 在交换机块中声明的局部变量或常量的范围是开关块。

如果以下至少一个为 true,则可访问开关标签:

  • switch 表达式是一个常量值,并且
    • 标签是一个 case 模式,其模式 将匹配 该值(§11.2.1),并且标签的防护要么不存在,要么不是值为 false 的常量表达式;或者
    • 它是一个 default 标签,而且没有一个 switch 部分包含模式与该值匹配的大小写标签,其保护要么不存在,要么是一个值为 true 的常量表达式。
  • switch 表达式不是一个常量值,并且
    • 标签是一个不带保护的 case,或带有保护但保护的值不是常量 false;或者
    • 它是一个 default 标签,并且
      • 在 switch 语句的情况中,如果不存在保护或存在其值为常量 true 的保护,那么出现在这些情况中的模式集合对于 switch 控制类型来说并非详尽 (§11.4);或者
      • 如果 switch 控制类型是可为 null 的类型,并且在 switch 语句中没有保护或保护值为常量 true 的情况下出现的模式集合不包含与 null 值相匹配的模式。
  • switch 标签由可访问的 goto casegoto default 语句引用。

如果 switch 语句可访问,并且 switch 节包含可访问的开关标签,则给定 switch 节的语句列表是可访问的。

如果 switch 语句是可访问的,并且以下至少有一项为 true,则 switch 语句的终点是可访问的:

  • switch 语句包含一个可访问的 break 语句,而该语句会退出 switch 语句。
  • 不存在 default 标签,并且
    • switch 表达式是一个非常量值,对于 switch 管理类型而言,在 switch 语句的情况下,出现在没有保护或有其值为常量 true 的保护的情况下的模式集并不详尽 (§11.4)。
    • switch 表达式是一个可为 null 的类型的非常量值,在 switch 语句中没有保护或保护值为常量 true 的情况下出现的任何模式都不会与 null 值相匹配。
    • switch 表达式是一个常量值,没有保护或保护为常量 true 的 case 标签都不会与该值匹配。

示例:以下代码显示了子句的 when 简洁用法:

static object CreateShape(string shapeDescription)
{
   switch (shapeDescription)
   {
        case "circle":
            return new Circle(2);
        …
        case var o when string.IsNullOrWhiteSpace(o):
            return null;
        default:
            return "invalid shape description";
    }
}

var 事例匹配 null、空字符串或任何只包含空白的字符串。 结束示例

13.9 迭代语句

13.9.1 常规

迭代语句重复执行嵌入语句。

iteration_statement
    : while_statement
    | do_statement
    | for_statement
    | foreach_statement
    ;

13.9.2 while 语句

while 语句有条件地执行嵌入语句零次或多次。

while_statement
    : 'while' '(' boolean_expression ')' embedded_statement
    ;

while 语句的执行过程如下:

  • boolean_expression§12.24)进行求值。
  • 如果布尔表达式生成 true,则控件将传输到嵌入语句。 当控制流到达嵌入语句的终点(可能是由于执行了continue语句)时,控制流会传递到while语句的开头。
  • 如果布尔表达式生成 false,则控件将传输到语句的 while 终点。

在语句的 while 嵌入语句中, break 语句(§13.10.2)可用于将控制权转移到语句的 while 终点(从而结束嵌入语句的迭代),而 continue 语句(§13.10.3)可用于将控件传输到嵌入语句的终点(从而执行语句的另 while 一次迭代)。

如果while语句可访问且布尔表达式没有常量值false,则while语句的嵌入语句是可访问的。

如果以下条件至少有一个为 true,则 while 语句的终点是可访问的:

  • while 语句包含一个可访问的 break 语句,该语句退出了 while 语句。
  • while 语句是可访问的,布尔表达式没有常量值 true

13.9.3 do 语句

do 语句有条件地执行嵌入语句一次或多次。

do_statement
    : 'do' embedded_statement 'while' '(' boolean_expression ')' ';'
    ;

do 语句的执行过程如下:

  • 控制权将转移到嵌入语句。
  • 当控制到达嵌入语句的终点(可能是执行continue语句后),将对boolean_expression§12.24)求值。 如果布尔表达式生成 true,则控制权将传输到语句的 do 开头。 否则,控制权将转移到语句的 do 结束点。

在语句的 do 嵌入语句中, break 语句(§13.10.2)可用于将控制权转移到语句的 do 终点(从而结束嵌入语句的迭代),而 continue 语句(§13.10.3)可用于将控件传输到嵌入语句的终点(从而执行语句的另 do 一次迭代)。

如果 do 语句是可访问的,则 do 语句的嵌入语句也是可访问的。

一个 do 语句的终点是可访问的,如果下列条件中至少有一个为 true:

  • do语句包含一个可访问的break语句,该语句退出do语句。
  • 嵌入语句的终点是可访问的,布尔表达式没有常量值 true

13.9.4 for 语句

for 语句计算初始化表达式序列,然后,当条件为 true 时,重复执行嵌入语句并计算迭代表达式序列。

for_statement
    : 'for' '(' for_initializer? ';' for_condition? ';' for_iterator? ')'
      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)*
    ;

for_initializer如果存在,则由local_variable_declaration§13.6.2)或用逗号分隔的statement_expression列表(§13.7)组成。 一个通过for_initializer声明的局部变量的作用范围是for_initializerfor_conditionfor_iteratorembedded_statement

如果存在,for_condition 应为boolean_expression§12.24)。

for_iterator(如果存在)由一系列以逗号分隔的 statement_expression (§13.7) 组成。

for 语句的执行过程如下:

  • 如果存在for_initializer,则按写入顺序执行变量初始值设定项或语句表达式。 此步骤仅执行一次。
  • 如果存在for_condition,则会对其进行评估。
  • for_condition不存在或计算结果是true,则控制权将传输到嵌入语句中。 当控制到达嵌入语句的终点时(可能是由于执行了 continue 语句),将依次对 for_iterator 的表达式(如果有)求值,然后执行另一次迭代,从上述步骤中的 for_condition 求值开始。
  • 如果for_condition存在并且评估结果为false,控制将转移到for语句的终点。

for 语句的嵌入语句中,break 语句(§13.10.2)可用于将控制权转移到 for 语句的结束点(因此结束嵌入语句的迭代),而 continue 语句(§13.10.3)可用于将控制权转移到嵌入语句的结束点(因此执行 for_iterator 并让 for 语句再次迭代),从 for_condition 开始。

如果以下任一条件为 true,则 for 语句中的嵌入语句是可访问的:

  • for语句是可访问的,并且不存在for_condition
  • for语句是可访问的,并且存在for_condition,且不是常量值false

如果下面至少有一个为真,则 for 语句的终点是可访问的:

  • for 语句包含一个可访问的 break 语句,而该语句会退出 for 语句。
  • for 语句可以访问,并且有 for_condition 存在,而且它没有常量值 true

13.9.5 foreach 语句

foreach 语句枚举集合的元素,为集合的每个元素执行嵌入语句。

foreach_statement
    : 'foreach' '(' ref_kind? local_variable_type identifier 'in' 
      expression ')' embedded_statement
    ;

local_variable_type标识符声明foreach语句的迭代变量var如果标识符作为local_variable_type提供,并且没有命名var的类型在范围内,则迭代变量据说是隐式类型的迭代变量,其类型将被视为语句的foreach元素类型,如下所示。

如果foreach_statement同时包含refreadonly或者两者都不包含,那么迭代变量表示一个被视为只读的变量。 否则,如果foreach_statement包含ref但不包含readonly,那么该迭代变量表示一个应当是可写的变量。

迭代变量对应于一个局部变量,该局部变量的范围扩展了嵌入语句。 在执行 foreach 语句期间,迭代变量表示当前正在执行迭代的集合元素。 如果迭代变量表示只读变量,则当嵌入语句尝试修改它(通过赋值或运算符++)或将其作为引用或--输出参数传递时,会发生编译时错误。

在下面的命名空间中,为简洁起见, IEnumerableIEnumeratorIEnumerable<T>IEnumerator<T>引用命名空间System.CollectionsSystem.Collections.Generic中的相应类型。

编译时对foreach语句的处理首先确定表达式的集合类型枚举器类型迭代类型。 此决定按如下所述进行:

  • 如果表达式的类型X是数组类型,则从 X 到IEnumerable接口有隐式引用转换(因为System.Array实现此接口)。 集合类型是 IEnumerable 接口,枚举器类型是 IEnumerator 接口,迭代类型是数组类型的元素类型 X
  • 如果表达式的类型为dynamic,则存在从表达式IEnumerable接口的隐式转换(§10.2.10)。 集合类型是 IEnumerable 接口,枚举器类型是 IEnumerator 接口。 var如果标识符作为local_variable_type给定,则迭代类型为 dynamic,否则为 object
  • 否则,请确定类型 X 是否具有适当的 GetEnumerator 方法:
    • 使用标识符X和无类型参数对类型GetEnumerator执行成员查找。 如果成员查找未生成匹配项,或引起歧义,或者生成的匹配项不是方法组,请检查可枚举接口,如下所示。 建议在成员查找产生除方法组之外的任何结果或没有匹配项时发出警告。
    • 使用生成的方法组和空参数列表执行重载解析。 如果重载解析不会导致任何适用的方法、不明确或导致单一最佳方法,但该方法为静态或不公开方法,请检查是否具有可枚举接口,如下所示。 如果重载解析产生除明确的公共实例方法之外的任何结果,或者没有适用的方法,建议发出警告。
    • 如果方法的E返回类型GetEnumerator不是类、结构或接口类型,则会生成错误,并且不执行进一步的步骤。
    • E 上使用标识符 Current 执行成员查找,并且没有类型参数。 如果成员查找未找到匹配项,则结果为错误;或者结果是除允许读取的公共实例属性之外的任何内容,则会生成错误,并且不执行进一步的步骤。
    • 成员查找在 E 上使用标识符 MoveNext 执行,并且没有提供类型参数。 如果成员查找不生成匹配项,则结果为错误,或者结果为除方法组之外的任何内容,则会生成错误,并且不执行进一步的步骤。
    • 重载解析在具有空参数列表的方法组中执行。 如果重载解析没有产生任何适用的方法、不明确或导致单一最佳方法,但该方法是静态的或不公开的,或者返回类型不是 bool,则会生成错误,并且不会采取进一步的步骤。
    • 集合类型为 X,枚举器类型为 E,迭代类型为属性的类型 Current 。 该 Current 属性可能包括 ref 修饰符,在这种情况下,返回的表达式是一个 variable_reference (§9.5),该表达式可选为只读。
  • 否则,检查是否存在可枚举接口:
    • 如果在从XIEnumerable<Tᵢ>有隐式转换的所有类型Tᵢ中,存在一种唯一的类型T,使得T不是dynamic,并且对于所有其他Tᵢ,从IEnumerable<T>IEnumerable<Tᵢ>也存在隐式转换,那么集合类型是接口IEnumerable<T>,枚举器类型是接口IEnumerator<T>,而迭代类型是T
    • 否则,如果有多个此类类型 T,则会生成错误,并且不执行进一步的步骤。
    • 否则,如果有从XSystem.Collections.IEnumerable接口的隐式转换,则集合类型为System.Collections.IEnumerable接口,枚举器类型为System.Collections.IEnumerator接口,迭代类型为object接口。
    • 否则,将生成错误,并且不采取进一步的步骤。

上述步骤(如果成功)明确生成集合类型 C、枚举器类型和 E 迭代类型 Tref Tref readonly 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(迭代类型)到Vforeach语句中的local_variable_type)的显式转换(§10.3),则会产生错误并且不会执行进一步的步骤。

当迭代变量是引用变量(§9.7)时,使用形式为 foreach 的语句

foreach (ref V v in x) «embedded_statement»

然后等效于:

{
    E e = ((C)(x)).GetEnumerator();
    try
    {
        while (e.MoveNext())
        {
            ref V v = ref e.Current;
            «embedded_statement»
        }
    }
    finally
    {
        ... // Dispose e
    }
}

该变量 e 对表达式 x 、嵌入语句或任何其他程序的源代码不可见或可访问。 引用变量 v 在嵌入式语句中是可读写的,但 v 不应被重新 ref 分配(§12.21.3)。 如果从T(迭代类型)到Vlocal_variable_typeforeach语句中)的标识转换(§10.2.2)不存在,则会产生错误,并且不会执行进一步的步骤。

形式为 foreach (ref readonly V v in x) «embedded_statement»foreach 语句具有类似的等效形式,但引用变量 v 在嵌入式语句中为 ref readonly,因此不能重新赋值或再赋值。

注意:如果 x 具有值 null,则会在运行时引发 a System.NullReferenceException结束备注

允许实现以不同的方式实现给定 foreach_statement ;例如,出于性能原因,只要行为与上述扩展一致。

vwhile循环内部的位置对于在embedded_statement中发生的任何匿名函数如何捕获它(§12.19.6.2)而言非常重要。

示例:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null)
    {
        f = () => Console.WriteLine("First value: " + value);
    }
}
f();

如果在 while 循环之外声明扩展形式的 v,那么它将在所有迭代中共享,在 for 循环之后,它的值将是最终值 13,也就是调用 f 时要打印的内容。 相反,因为每次迭代都有自己的变量 v,因此在第一次迭代中捕获 f 的值将继续保留值,这就是要打印的值 7。 (请注意,早期版本的 C# 在 while 循环之外声明了 v。)

结束示例

finally 块的主体是按照以下步骤构造的:

  • 如果存在从ESystem.IDisposable接口的隐式转换,则

    • 如果 E 是不可为空的值类型,则 finally 子句将扩展为语义等效项:

      finally
      {
          ((System.IDisposable)e).Dispose();
      }
      
    • 否则,finally 子句扩展为语义等同的内容:

      finally
      {
          System.IDisposable d = e as System.IDisposable;
          if (d != null)
          {
              d.Dispose();
          }
      }
      

      但如果 E 是一个值类型,或者是一个实例化为值类型的类型参数,则 eSystem.IDisposable 的转换不应导致装箱。

  • 否则,如果 E 为封闭类型,则 finally 子句会被扩展为空块:

    finally {}
    
  • 否则,finally 条款将扩展为:

    finally
    {
        System.IDisposable d = e as System.IDisposable;
        if (d != null)
        {
            d.Dispose();
        }
    }
    

d 局部变量对于任何用户代码都是不可见且不可访问的。 具体而言,它不会与范围包括 finally 块的任何其他变量冲突。

遍历数组元素的顺序 foreach 如下:对于单维数组元素,按索引顺序遍历,从索引 0 开始,以索引 Length – 1结尾。 对于多维数组,遍历元素的方式为:首先增加最右边维度的索引,然后是它左边的一个维度,以此类推,向左遍历元素。

示例:以下示例按元素顺序输出二维数组中的每个值:

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($"{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 的迭代类型。

结束示例

13.10 Jump 语句

13.10.1 常规

Jump 语句无条件转移控制。

jump_statement
    : break_statement
    | continue_statement
    | goto_statement
    | return_statement
    | throw_statement
    ;

jump 语句传输控件的位置称为 jump 语句的目标

当跳转语句发生在块内,而跳转语句的目标位于该块之外时,将表示跳转语句退出该块。 虽然 jump 语句可以将控制权从块中转移出来,但它永远无法将控制权转移到块中。

跳转语句的执行因存在干预 try 语句而变得复杂。 在没有此类 try 陈述的情况下,跳转语句无条件地将控制权从跳转语句转移到其目标。 在存在此类干预 try 语句的情况下,执行更为复杂。 如果 jump 语句退出了一个或多个带有相关 finally 块的 try 块,控制权最初会转移到最内层 try 语句的 finally 块。 当控制权到达 finally 块的终点时,控制权将转移到下一个封闭 try 语句的 finally 块。 此过程会重复进行,直到执行完所有中间 try 语句的 finally 块。

示例:在以下代码中

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");
    }
}

在控制权转移到 jump 语句的目标之前,与两个 try 语句相关的 finally 块将被执行。 生成的输出如下所示:

Before break
Innermost finally block
Outermost finally block
After break

结束示例

13.10.2 break 语句

break 语句退出最近的封闭 switchwhiledoforforeach 语句。

break_statement
    : 'break' ';'
    ;

break 语句的目标是最近的封闭 switchwhiledoforforeach 语句的终点。 如果break语句没有被switchwhiledoforforeach语句括起来,则会发生编译时错误。

当多个switchwhiledoforforeach语句相互嵌套时,语句break仅适用于最内部的语句。 若要跨多个嵌套级别传输控制, goto 应使用语句(§13.10.4)。

break 语句不能退出 finally 块 (§13.11)。 break当一条语句在finally块内发生时,该语句的目标break必须在同一个finally块中,否则会产生编译时错误。

break 语句的执行过程如下:

  • 如果 break 语句退出了一个或多个带有相关 finally 块的 try 块,控制权最初会转移到最内层 try 语句的 finally 块。 当控制权到达 finally 块的终点时,控制权将转移到下一个封闭 try 语句的 finally 块。 此过程会重复进行,直到执行完所有中间 try 语句的 finally 块。
  • 控制权将移交给break语句的目标。

break由于语句无条件地将控制权转移到别处,因此break语句的终点永远无法到达。

13.10.3 continue 语句

continue 语句会开始最接近的封闭 whiledoforforeach 语句的新迭代。

continue_statement
    : 'continue' ';'
    ;

continue 语句的目标是最近的封闭 whiledoforforeach 语句的嵌入语句的终点。 如果continue语句未被whiledoforforeach语句括起来,将会导致编译时错误。

当多个 while语句、 do语句 forforeach 语句相互嵌套时,语句 continue 仅适用于最内部的语句。 若要跨多个嵌套级别传输控制, goto 应使用语句(§13.10.4)。

continue语句无法退出finally块(§13.11)。 当continue语句发生在finally块中时,该continue语句的目标应位于同finally一个块内,否则会发生编译时错误。

continue 语句的执行过程如下:

  • 如果 continue 语句退出了一个或多个带有相关 finally 块的 try 块,控制权最初会转移到最内层 try 语句的 finally 块。 当控制权到达 finally 块的终点时,控制权将转移到下一个封闭 try 语句的 finally 块。 此过程会重复进行,直到执行完所有中间 try 语句的 finally 块。
  • 控制权会被转移到 continue 语句的目标。

continue由于语句无条件地将控制权转移到别处,因此continue语句的终点永远无法到达。

13.10.4 goto 语句

goto 语句会将控制权转交给带有标签的语句。

goto_statement
    : 'goto' identifier ';'
    | 'goto' 'case' constant_expression ';'
    | 'goto' 'default' ';'
    ;

gotoidentifier 语句的目标是带有给定标签的标记语句。 如果当前函数成员中不存在具有给定名称的标签,或者 goto 该语句不在标签范围内,则会发生编译时错误。

注意:此规则允许使用goto语句将控制权从嵌套范围转移,但不允许转移到嵌套作用域中。 在示例中

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($"{str} not found");
            continue;
          done:
            Console.WriteLine($"Found {str} at [{row}][{colm}]");
        }
    }
}

语句 goto 用于将控制权从嵌套范围转移出去。

尾注

goto case 语句的目标是紧接其后的 switch 语句 (§13.8.3) 中的语句列表,该语句包含一个 case 标签,该标签的常量模式为给定的常量值且无保护。 如果 goto case 语句没有被 switch 语句括起来,如果最近的括起来的 switch 语句不包含此类 case,或者如果 constant_expression 不能隐式地转换 (§10.2) 为最近的括起来的 switch 语句的控制类型,就会发生编译时错误。

一个 `goto default` 语句的目标是其直接包含的 `switch` 语句(§13.8.3)中的包含 `default` 标签的语句列表。 goto default如果语句未由switch语句括起来,或者最近的封闭switch语句不包含default标签,则会发生编译时错误。

goto 语句不能退出 finally 块 (§13.11)。 goto语句发生在finally块内时,goto语句的目标必须在同一finally块内,否则会发生编译时错误。

goto 语句的执行过程如下:

  • 如果 goto 语句退出了一个或多个带有相关 finally 块的 try 块,控制权最初会转移到最内层 try 语句的 finally 块。 当控制权到达 finally 块的终点时,控制权将转移到下一个封闭 try 语句的 finally 块。 此过程会重复进行,直到执行完所有中间 try 语句的 finally 块。
  • 控制权会被转移到 goto 语句的目标。

goto由于语句无条件地将控制权转移到别处,因此goto语句的终点永远无法到达。

13.10.5 return 语句

return 语句将控制返回给当前调用该函数成员的调用方,可选择性地返回一个值或一个 变量引用§9.5)。

return_statement
    : 'return' ';'
    | 'return' expression ';'
    | 'return' 'ref' variable_reference ';'
    ;

不含 expressionreturn_statement 被称为 return-no-value;如果它包含 refexpression,则被称为 return-by-ref;如果它仅包含 expression,则被称为 return-by-value

从声明为按值返回或按引用返回 (§15.6.1) 的方法中使用无返回值是一个编译时错误。

从声明为无值返回或按值返回的方法中使用按引用返回是一个编译时错误。

从声明为无值返回或按引用返回的方法中使用按值返回是一个编译时错误。

如果 expression 不是 variable_reference 或者是对 ref-safe-context 不是调用方上下文的变量的引用,那么使用按引用返回就是一个编译时错误 (§9.7.2)。

在使用 method_modifierasync 声明的方法中使用按引用返回是一个编译时错误。

如果函数成员是一个按值返回的方法 (§15.6.11)、一个属性或索引器的按值返回获取访问器,或者是一个用户定义的运算符,则将该函数成员称为计算值。 无返回值的函数成员不计算值,是有效返回类型为 void 的方法、属性和索引器的设置取值函数、事件的添加和删除取值函数、实例构造函数、静态构造函数和终结器。 按 ref 返回的函数成员不计算值。

对于按值返回,必须存在从表达式的类型到包含函数成员的有效返回类型(§15.6.11)的隐式转换(§10.2)。 对于按引用返回,expression 的类型和包含函数成员的有效返回类型之间应存在标识转换 (§10.2.2)。

return 语句也可以在匿名函数表达式(§12.19)的正文中使用,并参与确定这些函数存在哪些转换(§10.7.1)。

finally 块 (§13.11) 中出现 return 语句是一个编译时错误。

return 语句的执行过程如下:

  • 对于按值返回,expression 将被求值,其值将通过隐式转换转换为包含函数的有效返回类型。 转换的结果将成为函数生成的结果值。 对于通过引用返回的情况,表达式会被计算,其结果应被归类为一个变量。 如果封闭方法的 return-by-ref 包含 readonly,则生成的变量为只读。
  • 如果return语句被一个或多个包含关联finally块的trycatch块所括起来,控制最初将传递到最内层try语句的finally块。 当控制权到达 finally 块的终点时,控制权将转移到下一个封闭 try 语句的 finally 块。 此过程会重复进行,直到执行完所有封闭 try 语句的 finally 块。
  • 如果包含函数不是异步函数,则控件将返回给包含函数的调用方以及结果值(如果有)。
  • 如果包含函数是异步函数,则控件将返回到当前调用方,并且结果值(如果有)将记录在返回任务中(§15.15.3)。

return由于语句无条件地将控制权转移到别处,因此return语句的终点永远无法到达。

13.10.6 throw 语句

throw 语句引发异常。

throw_statement
    : 'throw' expression? ';'
    ;

带有表达式的 throw 语句会在对表达式求值时引发异常。 表达式应能隐式转换为 System.Exception,其求值结果将在被抛出之前转换为 System.Exception。 如果转换结果是 null,则会引发一个 System.NullReferenceException

catch块中,只能使用没有表达式的throw语句。在这种情况下,该语句将重新抛出当前由catch块正在处理的异常。

throw由于语句无条件地将控制权转移到别处,因此throw语句的终点永远无法到达。

当引发异常时,控制权会转移到可以处理异常的封闭 try 语句中的第一个 catch 子句。 从引发的异常点到将控件传输到适当的异常处理程序的点的过程称为异常传播 异常的传播过程包括重复评估以下步骤,直到找到与异常匹配的catch子句。 在此说明中, 引发点 最初是引发异常的位置。 此行为在 (§21.4) 中指定。

  • 在当前函数成员中,将检查每个包围引发点的 try 语句。 对于每个语句 S,从最 try 内部的语句开始,以最 try 外部的语句结尾,将评估以下步骤:

    • 如果tryS封闭了引发点,并且S包含一个或多个catch子句,那么将按照它们出现的顺序检查catch子句,以便为异常找到合适的处理程序。 如果第一个 catch 子句指定了异常类型 T(或运行时表示异常类型 T 的类型参数),且 E 的运行时类型派生自 T,则会被视为匹配。 如果该子句包含异常筛选器,则异常对象将分配给异常变量,并计算异常筛选器。 当catch子句包含异常筛选器,并且异常筛选器的计算结果为true时,catch该子句被认为是匹配的。 一般 catch (§13.11) 子句被视为任何异常类型的匹配项。 如果找到匹配 catch 子句,则通过将控制权传输到该 catch 子句块来完成异常传播。
    • 否则,如果 try 块或 catchS 包含引发点,并且 S 具有 finally 块,则将控制权传递到 finally 块。 如果 finally 块引发另一个异常,则终止处理当前异常。 否则,当控制到达 finally 块的终点时,将继续处理正在处理的异常。
  • 如果异常处理程序未位于当前函数调用中,函数调用将终止,并发生以下情况之一:

    • 如果当前函数为非异步函数,则对函数的调用方重复上述步骤,其引发点对应于调用函数成员的语句。

    • 如果当前函数是异步且返回任务的,那么异常会记录在返回的任务中,该任务将进入错误或取消状态,如§15.15.3中所述。

    • 如果当前函数是异步的并且返回 void,那么将按照 §15.15.4 中所述通知当前线程的同步上下文。

  • 如果异常处理终止当前线程中的所有函数成员调用,指示该线程没有异常的处理程序,则线程本身将终止。 此类终止的影响由执行情况决定。

13.11 try 语句

try 语句提供了一种在执行代码块期间捕获异常的机制。 此外,该 try 语句提供指定在控件离开 try 语句时始终执行的代码块的功能。

try_statement
    : 'try' block catch_clauses
    | 'try' block catch_clauses? finally_clause
    ;

catch_clauses
    : specific_catch_clause+
    | specific_catch_clause* general_catch_clause
    ;

specific_catch_clause
    : 'catch' exception_specifier exception_filter? block
    | 'catch' exception_filter block
    ;

exception_specifier
    : '(' type identifier? ')'
    ;

exception_filter
    : 'when' '(' boolean_expression ')'
    ;

general_catch_clause
    : 'catch' block
    ;

finally_clause
    : 'finally' block
    ;

try_statement 由关键字 tryblock 组成,然后是零个或多个 catch_clauses 以及一个可选的 finally_clause。 应至少有一个catch_clausefinally_clause

exception_specifier中,类型(或当它是type_parameter时的其有效基类)应为System.Exception或派生自该基类的类型。

catch当子句同时指定class_type标识符时,将声明给定名称和类型的异常变量。 异常变量将引入specific_catch_clause§7.3)的声明空间中。 在执行 exception_filtercatch 块期间,异常变量表示当前正在处理的异常。 为了进行明确的赋值检查,异常变量被视为在其整个范围内明确分配。

除非子 catch 句包含异常变量名称,否则无法访问筛选器和 catch 块中的异常对象。

不指定异常类型和异常变量名称的 catch 子句称为通用 catch 子句。 语句 try 只能有一个常规子句,如果存在一个语句 catch ,则它应该是最后 catch 一个子句。

注意:某些编程语言可能支持无法表示为派生 System.Exception对象的异常,尽管 C# 代码无法生成此类异常。 通用 catch 子句可用于捕获此类异常。 因此,一般的 catch 子句与指定 System.Exception 类型的子句在语义上是不同的,因为前者也可能捕获其他语言的异常。 尾注

为了找到异常处理程序,catch 子句会按词法顺序进行检查。 catch如果一个子句指定了类型但没有异常筛选器,那么对于同一try语句中后续的catch子句来说,指定与该类型相同或派生自该类型的类型将会引发编译时错误。

注意:如果没有此限制,就有可能编写出无法访问的 catch 子句。 结束注释

catch 块中,可以使用不带表达式的 throw 语句 (§13.10.6) 来重新引发 catch 块捕获的异常。 对异常变量的赋值不会更改重新引发的异常。

示例:在以下代码中

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

结束示例

如果 breakcontinuegoto 语句将控制权转出 finally 块,则是一个编译时错误。 当 breakcontinuegoto 语句出现在 finally 块中时,语句的目标应在同一 finally 块中,否则会出现编译时错误。

finally 块中出现 return 语句是一个编译时错误。

当执行到达 try 语句时,控制权将被转移到 try 块。 如果控件到达块的 try 终点,而不会传播异常,则控件将传输到 finally 块(如果存在)。 如果不存在 finally 块,则控制权将传输到语句的 try 终点。

如果异常已被传播,则按词法顺序检查 catch 子句(如果有),以寻找与异常匹配的第一个子句。 如 §13.10.6 所述,在所有封闭块中继续搜索匹配的 catch 子句。 如果异常类型匹配任何exception_specifier,且任何exception_filter为真,则catch子句是匹配项。 不含 exception_specifiercatch 子句可匹配任何异常类型。 当exception_specifier指定异常类型或异常类型的基类型时,异常类型与exception_specifier匹配。 如果该子句包含异常筛选器,则异常对象将分配给异常变量,并计算异常筛选器。

如果异常已被传播,并且找到了匹配的 catch 子句,则控制权将转移到第一个匹配的 catch 块。 如果控件到达块的 catch 终点,而不会传播异常,则控件将传输到 finally 块(如果存在)。 如果不存在 finally 块,则控制权将传输到语句的 try 终点。 如果已从 catch 块传播异常,则控件会传输到 finally 块(如果存在)。 异常会被传播到下一个封闭 try 语句。

如果已传播异常,并且找不到匹配 catch 子句,则控制权会转移到 finally 块(如果存在)。 异常会被传播到下一个封闭 try 语句。

当控制权离开 try 语句时,会始终执行 finally 块的语句。 无论控制转移是正常执行的结果,还是执行 breakcontinuegotoreturn 语句的结果,或是从 try 语句传播异常的结果,情况都是如此。 如果控制到达finally块的终点而没有传播异常,则控制将转移到try语句的终点。

如果在执行 finally 块期间出现异常,但未在同一 finally 块内捕获,则异常会传播到下一个封闭 try 语句。 如果在传播过程中存在另一个异常,该异常将丢失。 在throw语句的描述(§13.10.6)中,进一步讨论了异常传播的过程。

示例:在以下代码中

public class Test
{
    static void Main()
    {
        try
        {
            Method();
        }
        catch (Exception ex) when (ExceptionFilter(ex))
        {
            Console.WriteLine("Catch");
        }

        bool ExceptionFilter(Exception ex)
        {
            Console.WriteLine("Filter");
            return true;
        }
    }

    static void Method()
    {
        try
        {
            throw new ArgumentException();
        }
        finally
        {
            Console.WriteLine("Finally");
        }
    }
}

该方法 Method 引发异常。 第一个操作是检查封闭 catch 子句,执行任何异常筛选器。 然后,Method 中的 finally 子句在控制权转移到外层匹配的 catch 子句之前执行。 生成的输出为:

Filter
Finally
Catch

结束示例

如果try语句是可访问的,那么try语句的try块就是可访问的。

如果 try 语句是可访问的,则 try 语句的 catch 块也是可访问的。

如果 try 语句是可访问的,则 try 语句的 finally 块也是可访问的。

语句 try 的终点是可访问的,若下述两个条件均为 true:

  • try 块的终点是可访问的,或者至少一个 catch 块的终点是可访问的。
  • 如果finally块存在,则finally块的终点是可以到达的。

13.12 checked 和 unchecked 语句

checkedunchecked 语句用于控制整型算术运算和转换的溢出检查上下文

checked_statement
    : 'checked' block
    ;

unchecked_statement
    : 'unchecked' block
    ;

checked语句会导致块中的所有表达式在选中的上下文中求值,并且该unchecked语句会导致在未选中的上下文中计算块中的所有表达式。

checkedunchecked语句与checkedunchecked运算符(§12.8.20)完全相同,只是它们对块而不是表达式进行操作。

13.13 lock 语句

lock 语句获取给定对象的互斥锁,执行语句,然后释放锁。

lock_statement
    : 'lock' '(' expression ')' embedded_statement
    ;

lock 语句的 expression 应表示已知为 reference 类型的值。 lock 语句的 expression 不会进行隐式装箱转换 (§10.2.9),因此表达式表示 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 只计算一次。

当保留互斥锁时,在同一执行线程中执行的代码也可以获取和释放锁。 但是,在其他线程中执行的代码会被阻止获取锁,直到锁被释放。

13.14 using 语句

using 语句获取一个或多个资源,执行语句,然后释放资源。

using_statement
    : 'using' '(' resource_acquisition ')' embedded_statement
    ;

resource_acquisition
    : local_variable_declaration
    | expression
    ;

资源是实现System.IDisposable接口的类或结构,其中包括名为Dispose单个无参数方法的类或结构。 使用资源的代码可以调用 Dispose 以指示不再需要资源。

如果resource_acquisition的形式是local_variable_declaration,则local_variable_declaration的类型应为dynamic或可隐式转换为System.IDisposable的类型。 如果resource_acquisition的形式是表达式,则此表达式应隐式转换为System.IDisposable

resource_acquisition中声明的局部变量为只读属性,并且应包含初始化器。 如果嵌入语句尝试修改这些局部变量(通过赋值或使用 ++-- 运算符),或者获取它们的地址,或将它们作为引用参数或输出参数传递,则会发生编译时错误。

语句 using 被翻译为三个部分:购置、使用和处置。 资源的使用隐式包围在一个包含 finally 子句的 try 语句中。 此 finally 子句将处置该资源。 如果获取了null资源,则不会调用Dispose,也不会引发异常。 如果资源的类型是 dynamic,则在获取期间将其通过隐式动态转换(§10.2.10)动态转换为 IDisposable,以确保转换在使用和处置之前成功。

形式如下的 using 语句

using (ResourceType resource = «expression» ) «statement»

对应于三个可能的扩展之一。 如果 ResourceType 为不可为 null 的值类型或具有值类型约束的类型参数(§15.2.5),则扩展在语义上等效于

{
    ResourceType resource = «expression»;
    try
    {
        «statement»;
    }
    finally
    {
        ((IDisposable)resource).Dispose();
    }
}

但将 resource 转换为 System.IDisposable 不会导致装箱发生。

否则,当 ResourceTypedynamic 时,扩展为

{
    ResourceType resource = «expression»;
    IDisposable d = resource;
    try
    {
        «statement»;
    }
    finally
    {
        if (d != null)
        {
            d.Dispose();
        }
    }
}

否则,扩展为

{
    ResourceType resource = «expression»;
    try
    {
        «statement»;
    }
    finally
    {
        IDisposable d = (IDisposable)resource;
        if (d != null)
        {
            d.Dispose();
        }
    }
}

在任何扩展中,变量 resource 在嵌入语句中是只读的,并且该 d 变量不可访问,并且对嵌入语句不可见。

只要行为与上述扩展一致,就允许实现给定 using_statement 不同的方式,例如出于性能原因。

形式如下的 using 语句:

using («expression») «statement»

也有三种可能的扩展。 在这种情况下,如果表达式具有编译时类型,则ResourceType隐式为表达式的编译时类型。 否则,接口 IDisposable 本身将用作 ResourceType. 该 resource 变量不可访问,并且对嵌入 语句不可见。

当一个资源获取采用局部变量声明的形式时,可以获取多个给定类型的资源。 形式如下的 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的文件,并将两行文本写入文件。 然后,该示例打开同一个文件,用于读取和将包含的文本行复制到控制台。

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);
            }
        }
    }
}

TextWriterTextReader类实现了IDisposable接口,因此该示例可以使用using语句来确保基础文件在写入或读取操作后正确关闭。

结束示例

13.15 yield 语句

yield语句用于迭代器块(§13.3)向枚举器对象(§15.14.5)或可枚举对象(§15.14.6)生成值,或发出迭代结束信号。

yield_statement
    : 'yield' 'return' expression ';'
    | 'yield' 'break' ';'
    ;

yield是一个上下文关键字(§6.4.4),仅在紧邻returnbreak关键字之前使用时,才具有特殊含义。

语句 yield 可以出现的位置存在一些限制,如下所述。

  • method_bodyoperator_bodyaccessor_body之外出现yield语句(任一形式)时,会导致编译时错误。
  • 匿名函数中出现yield语句(任一形式)会导致编译时错误。
  • try 语句的 finally 子句中出现 yield 语句(无论哪种形式)都是编译时错误。
  • 在包含任何 catch_clausestry 语句中的任何位置出现 yield return 语句都是编译时错误。

示例:以下示例显示了语句的一些有效和无效用法 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 语句中表达式的类型到迭代器的 yield 类型 (§15.14.4) 之间应存在隐式转换 (§10.2)。

执行语句 yield return 的方式如下:

  • 给定语句中计算出的表达式,会被隐式转换为 yield 类型,并赋值给枚举器对象的 Current 属性。
  • 迭代器块的执行已暂停。 yield return如果语句位于一个或多个try块内,则此时不会执行关联的finally
  • 枚举器对象的方法将true返回给其调用方,表明枚举器对象已成功前进到下一项。

对枚举器对象方法的 MoveNext 下一次调用将从上次暂停的位置恢复迭代器块的执行。

yield break 语句的执行过程如下:

  • 如果 yield break 语句被一个或多个带有相关 finally 块的 try 块包围,控制权最初会转移到最内层 try 语句的 finally 块。 当控制权到达 finally 块的终点时,控制权将转移到下一个封闭 try 语句的 finally 块。 此过程会重复进行,直到执行完所有封闭 try 语句的 finally 块。
  • 控件将返回到迭代器块的调用方。 这要么是枚举器对象的 MoveNext 方法,要么是 Dispose 方法。

yield break由于语句无条件地将控制权转移到别处,因此yield break语句的终点永远无法到达。