标记
注释
本文是功能规格说明。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 ECMA 规范。
功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的 语言设计会议(LDM)记录中。
可以在有关 规范的文章中详细了解将功能规范采用 C# 语言标准的过程。
总结
允许 break 和 continue 语句(可选)指定一个标签,用于标识要面向的循环或 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);
此方法需要额外的状态管理、增加代码详细程度并掩盖控制流意向。
建议的解决方案
使用标记 break 后 continue,代码变得更加清晰且更易于维护:
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# 社区广泛要求此功能,讨论可追溯到几十年,主题不断重新引入和重新请求。 其他几种新式语言中也存在类似的功能:
- Java:分支语句(Oracle 教程)
- JavaScript: 标记语句 (MDN)
- Kotlin: 返回和跳跃
- Swift: 控制流 - 标记语句
- Rust: 循环标签
- Go: 标记的语句
- Zig: 带标签的循环
- 达特: 循环
在所有这些情况下,语言的工作方式与本规范相同。 也就是说,某些构造可以具有标签,并且可以从各自的 continue 或 break 语句引用该标签。
详细设计
以下更新针对 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_statement 或 iteration_statement。 因此, break a; 或 continue a; 出现在循环正文中并不针对 while 该语句。
尾注
§13.10.2 中断语句
break_statement
: 'break' identifier? ';'
;
该break语句退出最接近的switch封闭、while、do或forforeach语句。
该 break 语句退出最接近的封闭 switch_statement (§13.8.3)或 iteration_statement (§13.9),或者,如果指定 了标识符 ,则最接近的封闭 switch_statement 或 iteration_statement 标有该 标识符 (请参阅 §13.5)。
语句的目标break是最接近的封闭的终点,该语句确定为上面。
switch、while、do或forforeach语句语句
如果不存在此类封闭语句,则会发生编译时错误。break如果语句未由编译 whileforeachfordo时错误括起来switch,则会发生编译时错误。
当多个switch、while、do或forforeach语句相互嵌套时,语句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语句开始对最近的封闭while、do或forforeach语句进行新的迭代。
该 continue 语句开始对最接近的封闭 iteration_statement (§13.9)进行新的迭代,或者,如果指定 了标识符 ,则最接近的封闭 iteration_statement 标有该 标识符 (请参阅 §13.5)。
语句的目标 continue 是最接近封闭 iteration_statement 如上所述。
while、 do或 for语句的嵌入语句的终点,或 foreach 语句
如果不存在此类封闭语句,则会发生编译时错误。continue如果语句未由编译doforforeach时错误括起来while,则会发生编译时错误。
当多个 while语句、 do语句 for或 foreach 语句相互嵌套时,语句 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 N 或 continue 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