ラベル付き break ステートメントと continue ステートメント

Note

この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。

機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。

機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。

まとめ

必要に応じて、 break ステートメントと continue ステートメントで、対象となるループまたは switch ステートメントを識別するラベルを指定できます。これにより、入れ子になったコンストラクト内の制御フローを、 goto ステートメントや、入れ子になった関数、タプルの戻り値などの他の制御を必要とせずに、よりクリーンに制御できます。

モチベーション

switchステートメントを含む入れ子になったループまたはループを操作する場合、開発者は内部コンテキスト内から外側のループから抜け出すか、外側のループを続行する必要があります。 現時点では、これを実現するための主なアプローチが 2 つあります。両方に大きな欠点があります。

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

このパターンは、インクリメントと条件のチェックが行われるように、閉じかっこの直前のループ本体の末尾にラベルを配置する必要があるため、混乱を招きます。 同じ外部ループに breakcontinue の両方が必要な場合は、2 つの異なるラベルが必要です。

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に関連付けられたラベルの配置を混乱させることなく、自然に "外側のループを続ける" を表します。 1 つのラベルを両方の操作に使用できます。

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# コミュニティで広く要求されており、数十年に及ぶ議論が行われ、トピックは継続的に再導入され、再要求されています。 同様の機能は、他のいくつかの最新言語に存在します。

いずれの場合も、言語はこの仕様と同じように動作します。 つまり、一部のコンストラクトはラベルを持つ可能性があり、それぞれの 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 (…) …を指定すると、iteration_statementにラベルを付けるのはbだけです。a内部のlabeled_statementにラベルを付けますb: while (…) …、それ自体はswitch_statementまたはiteration_statementではありません。 したがって、ループ本体内に表示される break a; または continue a; は、 while ステートメントを対象としません。 注釈

§13.10.2 break ステートメント

break_statement
    : 'break' identifier? ';'
    ;

break ステートメントは、最も近い外側のswitchwhiledofor、またはforeachステートメントを終了します。

breakステートメントは、最も近い外側のswitch_statement (§13.8.3) またはiteration_statement (§13.9) を終了するか、識別子が指定されている場合は、その識別子で最も近い外側のswitch_statementまたはiteration_statementを終了します (§13.5 を参照)。

breakステートメントのターゲットは、最も近い外側のswitchwhiledofor、またはforeachステートメントステートメントのエンドポイントです。 break ステートメントがswitchwhiledofor、またはforeachステートメントで囲まれていない場合、コンパイル時エラーが発生します。このような囲みステートメントが存在しない場合は、コンパイル時エラーが発生します。

複数の switchwhiledofor、または foreach ステートメントが相互に入れ子になっている場合、 break ステートメントは最も内側のステートメントにのみ適用されます。 複数の入れ子レベル間で制御を転送するには、 goto ステートメント (§13.10.4) を使用する必要があります。

break ステートメントは、finally ブロック (§13.11) を終了できません。 break ブロック内でfinallyステートメントが発生した場合、break ステートメントのターゲットは同じfinally ブロック内に存在する必要があります。それ以外の場合は、コンパイル時エラーが発生します。

break ステートメントは次のように実行されます。

  • break ステートメントが関連付けられた try ブロックを持つ 1 つ以上の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 ステートメント

continue_statement
    : 'continue' identifier? ';'
    ;

continueステートメントは、最も近い外側のwhiledofor、またはforeachステートメントの新しいイテレーションを開始します。

continueステートメントは、最も近い外側のiteration_statement (§13.9) の新しいイテレーションを開始します。識別子が指定されている場合は、その識別子でラベル付けされた最も近い外側のiteration_statementが開始されます (§13.5 を参照)。

continueステートメントのターゲットは、最も近い外側のwhiledofor、またはforeachステートメントの埋め込みステートメントのエンドポイントiteration_statement上記のように決定されます。 continue ステートメントがwhiledofor、またはforeachステートメントで囲まれていない場合は、コンパイル時エラーが発生します。このような囲みステートメントが存在しない場合は、コンパイル時エラーが発生します。

複数の whiledofor、または foreach ステートメントが相互に入れ子になっている場合、 continue ステートメントは最も内側のステートメントにのみ適用されます。 複数の入れ子レベル間で制御を転送するには、 goto ステートメント (§13.10.4) を使用する必要があります。

continue ステートメントは、finally ブロック (§13.11) を終了できません。 continue ブロック内でfinallyステートメントが発生した場合、continue ステートメントのターゲットは同じfinally ブロック内に存在する必要があります。それ以外の場合は、コンパイル時エラーが発生します。

continue ステートメントは次のように実行されます。

  • continue ステートメントが関連付けられた try ブロックを持つ 1 つ以上の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 の例

欠点/代替

goto ステートメントを使用し続ける

C# では、同じ制御フローを実現できる gotoが既にサポートされています。 ただし、 goto には、ラベル付けされた break/continue と比較していくつかの欠点があります。

  • 中断シナリオと続行シナリオに個別のラベルが必要です (中断ラベルはループの後に、ラベルは右中かっこの前に移動します)
  • ラベルの配置は直感的でなく、中断しているか続行しているかによって異なります
  • 意図に関する明示的な情報が少ない (特定の場所へのジャンプと特定のループの中断/継続)
  • 脆弱でエラーが発生しやすい: 開発者は、ラベルとそのターゲット コンストラクトの間にステートメントが誤って配置されないようにする必要があります。 たとえば、 goto END_LOOP; の後に END_LOOP:が続いていると、メンテナンス中に誤ってステートメントを挿入し、意図した制御フローを中断しやすくなります。 ラベル付きループは、ラベルをコンストラクトに直接バインドすることで、この問題を回避します。
  • break/continue というラベルが付いた歴史的な汚名を含む

フラグ変数を使用する

動機セクションに示すように、フラグ変数は機能しますが、重要な定型句を追加し、制御フロー ロジックを隠します。

数値レベルで break N または continue N を使用する

  • リファクタリング中に脆弱 (ループ レベルを追加または削除するには、すべての数値参照を更新する必要があります)
  • 読みにくい (ターゲットを理解するにはレベルをカウントする必要があります)
  • 名前付きラベルよりも明示的ではない
  • 明確さの欠如 (1 から 0 ベース?

個別のメソッドにリファクタリングする

これは多くの場合、適切な方法ですが、常に実現可能または適切であるとは限りません。また、単純な制御フローである必要がある場合に不要な複雑さが生じることがあります。

この提案は、次のコミュニティディスカッションを統合して対処します。

質問を開く

ラベル セマンティクス

現在の仕様では、 break identifier/continue identifier のセマンティクスを、その識別子でラベル付けされた最も内側の適用可能なループ/スイッチコンストラクトを見つけ、標準の break/continue セマンティクスでディスパッチすることを定義しています。 代わりに、 break identifier/continue identifier が 'goto' と同じルールを使用してラベルを識別するとします。 また、ラベルに break/continue を囲むループ/スイッチ コンストラクトが直接含まれている場合、それは break/continue が適用されるループ/スイッチです。

どちらの形式も実質的に同一であり、同じプログラムセットを許可および禁止します。 この仕様で選択されたアプローチは、概念とリテラルの両方のシンプルさのために行われました。 ラベルのスコープ、または goto のように識別子のバインドをカバーする必要はありません。また、ループ/スイッチと continue/break ステートメントを解決するための外側のバインド ロジックを定義する必要はありません。 代わりに、中断/続行を指定して適切な外側のループ/スイッチを見つける単純な仕様言語を拡張するだけで、最も内側を越えて、その上の何かに拡張できます。

LDMがこのより厳密にgoto +ラベルセマンティクスを同じに感じるなら、スペックをそれに合わせるのは難しくありません。 グループが後者の形式がここで取ったフォームよりも自然であると感じる場合は、この質問を開いたままにします。

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を介して参照されます。 ブロックのラベル宣言スペースには、入れ子になったブロックが含まれています。 したがって、入れ子になったブロック内では、外側のブロック内のラベルと同じ名前のラベルを宣言することはできません。

ラベル付きステートメントの仕様:

ラベルのスコープは、入れ子になったブロックを含め、ラベルが宣言されているブロック全体です。 同じ名前の 2 つのラベルに重複するスコープがあると、コンパイル時エラーになります。
ラベルは、ラベルのスコープ内の goto ステートメント (§13.10.4) から参照できます。

入れ子になったラベル

サポート a: b: while (true) continue a; 必要がありますか?

推奨事項: いいえ。 ユーザーがこれを求めていません。 説得力のあるユース ケースは提示されていません。 ほとんどのメインストリーム言語は、コミュニティからの苦情なしで、それを許可していません。 lang (および impl) は、ラベル付けされたステートメントを含む直接のみがループ/スイッチにラベルを付ける、よりシンプルで明確な厳格をもたらします。

デザインに関する会議

TBD