标记 breakcontinue 语句

注释

本文是功能规格说明。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 ECMA 规范。

功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的 语言设计会议(LDM)记录中。

可以在有关 规范的文章中详细了解将功能规范采用 C# 语言标准的过程。

总结

允许 breakcontinue 语句(可选)指定一个标签,用于标识要面向的循环或 switch 语句,在嵌套构造中启用更清晰的控制流,而无需 goto 语句,或者其他构造(如嵌套函数、元组返回等)。

动机

使用包含 switch 语句的嵌套循环或循环时,开发人员通常需要从内部上下文中分解或继续外部循环。 目前,有两种主要方法可以实现此目的,两者都有明显的缺点:

Using goto 语句

string foundValue = null;
for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
            goto FOUND;
    }
}
FOUND:
ProcessValue(foundValue);

虽然 goto 有效,但它需要在循环构造后放置标签,并且不会明确传达要从特定循环中中断的意向。 对于继续外部循环,此方法变得更加尴尬:

for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            goto CONTINUE_OUTER;
    }
    CONTINUE_OUTER: ;
}

此模式令人困惑,因为标签必须放置在循环正文的末尾,就在右大括号之前,以便进行增量和条件检查。 当这两个外部循环都需要这 break 两个标签时 continue ,需要两个单独的标签:

for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            goto CONTINUE_OUTER;
        
        if (ShouldExitAll(x, y))
            goto BREAK_OUTER;
    }
    CONTINUE_OUTER: ;
}
BREAK_OUTER:
// Subsequent statements

使用标志变量

string foundValue = null;
bool shouldBreak = false;
for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
        {
            shouldBreak = true;
            break;
        }
    }
    if (shouldBreak)
        break;
}
ProcessValue(foundValue);

此方法需要额外的状态管理、增加代码详细程度并掩盖控制流意向。

建议的解决方案

使用标记 breakcontinue,代码变得更加清晰且更易于维护:

string foundValue = null;
outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
            break outer;
    }
}
ProcessValue(foundValue);

标签直接放置在它标识的循环上,break/continue 语句显式命名其目标。 对于继续:

outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            continue outer;
    }
}

这自然地表达了“继续外循环”,而不会混淆与 goto标签放置有关。 单个标签可用于这两个操作:

outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            continue outer;
        
        if (ShouldExitAll(x, y))
            break outer;
    }
}

C# 社区广泛要求此功能,讨论可追溯到几十年,主题不断重新引入和重新请求。 其他几种新式语言中也存在类似的功能:

在所有这些情况下,语言的工作方式与本规范相同。 也就是说,某些构造可以具有标签,并且可以从各自的 continuebreak 语句引用该标签。

详细设计

以下更新针对 C# 7 标准(statements.md)的相应部分进行了差异。 在整个部分中, 删除线 指示从现有规范中删除的文本, 加粗 表示正在添加的文本。 未更改的散文被引用为上下文逐字引用。

§13.5 标记语句

紧接在现有段落后面插入以下段落“标签可以在标签范围内从 goto 语句(§13.10.4)引用”。

如果labeled_statement中立即嵌套的语句switch_statement§13.8.3)或iteration_statement§13.9),则表示嵌套语句将被标记为labeled_statement标识符。 break_statement§13.10.2)或continue_statement§13.10.3)可以指定此类标识符来引用包含的语句。

注意:只有直接嵌套在labeled_statement中的语句才会标记该标识符。 例如,给定的情况下 a: b: while (…) …,仅 b 标记 iteration_statement; a 标记内部 labeled_statementb: while (…) …,它本身不是 switch_statementiteration_statement。 因此, break a;continue a; 出现在循环正文中并不针对 while 该语句。 尾注

§13.10.2 中断语句

break_statement
    : 'break' identifier? ';'
    ;

break语句退出最接近的switch封闭、whiledoforforeach语句。

break 语句退出最接近的封闭 switch_statement§13.8.3)或 iteration_statement§13.9),或者,如果指定 了标识符 ,则最接近的封闭 switch_statementiteration_statement 标有该 标识符 (请参阅 §13.5)。

语句的目标break是最接近的封闭switchwhiledoforforeach语句语句的终点,该语句确定为上面 break如果语句未由编译 whileforeachfordo时错误括起来switch,则会发生编译时错误。如果不存在此类封闭语句,则会发生编译时错误。

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

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

break 语句的执行过程如下:

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

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

示例:标记 break 的解析为最接近的封闭 switch_statement 或具有匹配标签 的iteration_statement

outer: for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (i * j > 20)
            break outer; // exits the outer for-loop
    }
}

end 示例

§13.10.3 继续语句

continue_statement
    : 'continue' identifier? ';'
    ;

continue语句开始对最近的封闭whiledoforforeach语句进行新的迭代。

continue 语句开始对最接近的封闭 iteration_statement§13.9)进行新的迭代,或者,如果指定 了标识符 ,则最接近的封闭 iteration_statement 标有该 标识符 (请参阅 §13.5)。

语句的目标 continue 是最接近封闭 whiledofor语句的嵌入语句的终点,或 foreach 语句iteration_statement 如上所述 continue如果语句未由编译doforforeach时错误括起来while,则会发生编译时错误。如果不存在此类封闭语句,则会发生编译时错误。

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

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

continue 语句的执行过程如下:

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

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

示例:带标签的 continue 标签解析为最接近的封闭 iteration_statement ,并带有匹配标签:

outer: for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (ShouldSkip(i, j))
            continue outer; // continues the outer for-loop
    }
}

end 示例

缺点/替代项

保留 using goto 语句

C# 已经支持 goto,它可以完成相同的控制流。 但是,与标记的中断/继续相比, goto 有几个缺点:

  • 需要单独的标签进行中断和继续方案(中断标签在循环后,继续标签在右大括号前继续)
  • 标签放置不太直观,具体取决于是中断还是继续
  • 不太明确意向(跳到位置与中断/继续特定循环)
  • 易出错:开发人员必须确保标签与其目标构造之间意外放置任何语句。 例如, goto END_LOOP;END_LOOP:维护期间,很容易无意中在它们之间插入语句,从而破坏预期的控制流。 标记循环通过将标签直接绑定到构造来阻止此问题。
  • 携带标记为中断/继续的历史耻辱可避免

使用标志变量

如“动机”部分所示,标记变量有效,但添加重要的样板并掩盖控制流逻辑。

使用 break Ncontinue N 用于数字级别

  • 重构期间易碎(添加/删除循环级别需要更新所有数值引用)
  • 更难阅读(必须计算级别才能理解目标)
  • 显式小于命名标签
  • 缺乏清晰度(从 1 开始?基于 0?)

重构为单独的方法

虽然这通常是一个很好的做法,但它并不总是可行或适当的,有时会引入不必要的复杂性,什么应该是简单的控制流。

此建议合并并解决以下社区讨论:

开放性问题

标签语义

当前规范定义语义,用于 break identifier/continue identifier 查找用该标识符标记的最内层适用的循环/开关构造,然后使用标准 break/continue 语义调度到该构造。 另一种形式化是说,使用 break identifier/continue identifier 与“goto”相同的规则标识标签。 如果标签直接包含包含中断/继续的循环/开关构造,则表示中断/继续应用于的循环/开关。

这两种形式化实际上都是相同的,允许和禁止相同的程序集。 在此幽灵中选择的方法既是为了概念性简单性,也为文字简单性而完成。 它不必涵盖标签的范围或标识符 goto 的绑定,也不必定义用于解析循环/开关并继续/中断语句的外向绑定逻辑。 相反,它只是扩展简单的规范语言,该语言查找适当的封闭循环/开关,给定中断/继续,允许它扩展到最内层,到上述内容。

如果 LDM 觉得将这种更紧密的标签语义绑在一起,那么将规范调整到这一点并不容易。 如果组觉得后一种形式比此处采用的形式更自然,请保持此问题的打开状态。

void M()
{
  label:
  Console.WriteLine();
  
  foreach (var x in ...)
  {
    break label;
    // should this scenario fail because:
    // 1. the identifier lookup fails, or
    // 2. the label is rejected (not a valid label for a `break` since not attached to a loop construct) after being found?
  }
}

规范 如何声明标签

每个块或switch_block为标签创建单独的声明空间。 名称通过 labeled_statements 引入此声明空间,并通过goto_statements引用名称。 块的标签声明空间包括任何嵌套块。 因此,在嵌套块中,不可能在封闭块中声明与标签同名的标签。

标记语句上的规范:

标签的范围是声明标签的整个块,包括任何嵌套块。 对于同名的两个标签来说,如果它们的范围重叠,这就是编译时错误。
可以在标签范围内从 goto 语句(§13.10.4)引用标签。

嵌套标签

应该 a: b: while (true) continue a; 支持吗?

建议:否。 没有用户要求这样做。 尚未针对它提出令人信服的用例。 大多数主流语言不允许这样做,没有来自社区投诉。 lang(和 impl)更简单、更清晰,仅包含带标签的语句的直接标签循环/开关。

设计会议

TBD