C#のbreak・continue・goto:多重ループの抜け方とgoto case

C#の無限ループとbreak・continue・gotoを.NET 10で動かし直し、多重ループを抜けるgoto・フラグ・ローカル関数を実測比較。switchのgoto caseとコンパイルエラーも整理しました。

2026年11月改訂: 2021年に韓国語で書いた「無限ループと制御文」の講座を、日本語向けに書き直しました。4つの例を .NET 10.0.14(C# 14)で動かし直すとすべて動きましたが、入力を合計する例は今のプロジェクトテンプレートで null 関連の警告が2つ(CS8600、CS8604) 出て、入力が途切れると ArgumentNullException で落ちました。本文の「無限ループはメモリが急に増える」「continue は条件のチェックへ直接進む」という説明も正確ではなかったので、実測とともに直しています。多重ループの抜け方の比較と、switch の goto case も新しく加えました。

C#でループを途中で止めたり飛ばしたりする方法を学んでいる人、そして二重ループを一気に抜けたくて goto を使っていいのか迷っている人向けの記事です。まとめると、break はいちばん内側のループか switch を1つだけ抜け、continue は今回の周回の残りを飛ばし、goto はラベルへ直接飛びます。 多重ループの抜け方は3通りとも速度が同じだったので、読みやすさで選べば大丈夫です。

無限ループ:使いどころと、実際に起きること

条件に true を書けば、ループは自分では終わりません。典型的なのは、入力を受け取り続けて特定の値が来たら break で抜ける形です。2021年の例は「end」と打つまで数を足し続けるプログラムで、net10.0 のプロジェクトに貼ってビルドすると警告が2つ出ます。

Program.cs(11,31): warning CS8600: Converting null literal or possible null value to non-nullable type.
Program.cs(15,34): warning CS8604: Possible null reference argument for parameter 's' in 'int int.Parse(string s)'.

今の dotnet new console のテンプレートでは <Nullable>enable</Nullable> が有効で、Console.ReadLine() は入力が閉じると null を返すからです。この警告は本物のバグを指していました。457、1006、422、14、33、end を入れると2021年と同じ1932が出ますが、end なしで入力が終わる(macOS・Linux なら Ctrl+D、Windows なら Ctrl+Z、またはパイプの終わり)と、こう落ちます。

Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 's')
   at System.Int32.Parse(String s)

12abc のような数でない入力では System.FormatException: The input string '12abc' was not in a correct format. です。両方に対応した2026年版はこうなります。

int sum = 0;
while (true)
{
    Console.Write("数を入力(endで終了): ");
    string? line = Console.ReadLine();
    if (line is null || line == "end") break;   // null = 入力が閉じた(Ctrl+D / Ctrl+Z、パイプの終わり)
    if (!int.TryParse(line, out int n))
    {
        Console.WriteLine($"  '{line}' は数ではないのでスキップ");
        continue;
    }
    sum += n;
}
Console.WriteLine($"合計: {sum}");

457、1006、12abc、422、14、33 をパイプで流し込むと、'12abc' は数ではないのでスキップ を1回出して 合計: 1932 で正常に終わりました。警告もゼロです。

2021年版には「うっかり無限ループになるとメモリが急速に増え、プログラムが落ちることがある」とありましたが、たいていはそうなりません。while (true) { n++; } を Release でビルドし、ps で10秒間見ていたところ、CPU は1コアを96〜100%使い続け、メモリ(RSS)は約34MBのまま変わりませんでした。 メモリが増えるのは、ループの中でリストに追加し続けるなど、何かを溜め込むときだけです。暴走ループの典型的な症状は「1コアが張り付いて応答しない」と覚えておくほうが正確です。

break と continue が実際に飛ぶ先

break は自分を囲むいちばん内側のループか switch を1つだけ抜けます。2021年の break の例(num が40になったら抜ける)は、num: 0 から num: 39 までちょうど40行を出力しました。

continue は今回の周回の残りを飛ばして次の周回へ進みます。1〜50の奇数を飛ばす例も、i: 2 から i: 50 までちょうど25行でした。

for ループの流れ図:初期化式のあと条件 i < n。真なら本体、反復式 i++ を経て条件へ戻る。偽ならループの次の文へ。本体からの点線:continue は i++ へ、break はループの次の文へ、goto Done は外側のループの後ろに置いたラベル Done へ向かう。
for ループの中で各ジャンプ文がどこへ飛ぶか。continue は本体の残りを飛ばすが反復式(i++)は実行する。break はいちばん内側のループか switch を抜ける。goto は外側のスコープのラベルへ飛び、3つの中で唯一、複数のループを一度に抜けられる。

2021年版は continue を「条件を調べる部分へ直接移る」と説明していましたが、for ループでは反復式(i++)を先に実行してから条件へ進みます。この違いが、while でよくあるバグの原因になります。

int i = 0, spins = 0;
while (i < 5)
{
    if (++spins > 20) { Console.WriteLine($"stuck at i={i} after {spins - 1} iterations"); break; }
    if (i == 2) continue;   // 下の i++ を永遠に飛ばす
    Console.WriteLine($"i={i}");
    i++;
}
for (int k = 0; k < 5; k++)
{
    if (k == 2) continue;   // for なら continue のあとも k++ が動く
    Console.Write($"k={k} ");
}
i=0
i=1
stuck at i=2 after 20 iterations
k=0 k=1 k=3 k=4

while 版は continue が i++ を飛ばすので i=2 で足踏みし(spins はデモ用の安全装置)、for 版は2だけ飛ばして最後まで進みました。while で continue を使うときは、増分を continue より上に置きましょう。

もう1つのよくある間違いが、ループの中の switch で書いた break です。この break は switch を抜けるだけで、ループは続きます。

string[] cmds = { "a", "quit", "b" };
foreach (string c in cmds)
{
    switch (c)
    {
        case "quit":
            Console.WriteLine("quit -> break");
            break;          // switch を抜けるだけ
        default:
            Console.WriteLine($"cmd {c}");
            break;
    }
}
Console.WriteLine("---");
foreach (string c in cmds)
{
    if (c == "quit") break;   // ループを抜ける
    Console.WriteLine($"cmd {c}");
}
cmd a
quit -> break
cmd b
---
cmd a

上のループは quit のあとの b まで処理し、下のループは quit で終わりました。

goto とラベルのルール

goto ラベル; は同じメソッドの中のラベルへ直接移ります。2021年の例は goto と EXIT; のあいだに空行が挟まった形でしたが、C# は空白や改行を気にしないので、そのままコンパイルでき、num: 39 のあとに「탈출!」(脱出!)を出力しました。コメントが「break で抜ける」のままなのは、break の例からコピーした名残です。

ラベルにはコンパイラが課すルールがいくつかあります。実際に出たエラーと警告です。

コード 結果
for ブロックの中のラベルへ外から goto Inside; error CS0159: No such label 'Inside' within the scope of the goto statement
ブロックの最後の行に End: だけ置いて } error CS1525: Invalid expression term '}'(+CS1002)
どこからも飛んでこないラベル warning CS0164: This label has not been referenced
goto の直後の文 warning CS0162: Unreachable code detected

つまり goto はブロックの外へは出られても、中へは入れません。ラベルのあとには必ず文が要るので、ブロックの最後に置くなら End: ; のように空の文を付けます。こうした制限のおかげで C# の goto は C 言語ほど危なくなく、実際の用途はほぼ「多重ループを一度に抜ける」と「switch の別の case へ移る」の2つです。

多重ループを抜ける3つの方法を比較

ジャグ配列で値を見つけたらすぐに両方のループを終えたい、という場面での3つの書き方です。

static (int, int) FindGoto(int[][] g, int target)
{
    (int, int) found = (-1, -1);
    for (int i = 0; i < g.Length; i++)
        for (int j = 0; j < g[i].Length; j++)
            if (g[i][j] == target) { found = (i, j); goto Done; }
    Done:
    return found;
}

static (int, int) FindFlag(int[][] g, int target)
{
    (int, int) found = (-1, -1);
    bool stop = false;
    for (int i = 0; i < g.Length && !stop; i++)
        for (int j = 0; j < g[i].Length; j++)
            if (g[i][j] == target) { found = (i, j); stop = true; break; }
    return found;
}

static (int, int) FindLocal(int[][] g, int target)
{
    return Search();

    (int, int) Search()   // ローカル関数:return 1回で両方のループが終わる
    {
        for (int i = 0; i < g.Length; i++)
            for (int j = 0; j < g[i].Length; j++)
                if (g[i][j] == target) return (i, j);
        return (-1, -1);
    }
}

[[3, 8, 1], [4, 7, 2], [9, 5, 6]] で7を探すと3つとも (1, 1)、ない値の42では (-1, -1) を返しました。速度も測りました。2,000×2,000 のジャグ配列の最後のマスに目的の値を置き、毎回400万マスを全部なめるようにして、100回呼んだ時間です(Release、.NET 10.0.14、Apple Silicon Mac、ウォームアップ後)。

方法 プロセス1(run 2 / run 3) プロセス2(run 2 / run 3)
goto Done 198.0 / 195.2 ms 192.7 / 191.5 ms
bool フラグ 201.8 / 197.1 ms 197.1 / 190.1 ms
ローカル関数 + return 195.4 / 195.1 ms 190.4 / 192.5 ms

各プロセスの最初の計測回は3つとも約260msでそろって遅く、JIT の最適化が終わる前だったと考えられます。そのあとの差は2%未満で、性能で選ぶ理由はありません。ローカル関数は g と target をキャプチャしますが、デリゲートに変換しないので構造体でキャプチャされ、1,000回呼んでも割り当ては0バイトでした。

判断図。最初の問い:抜けたいループはいくつか。1つなら break。複数なら次の問い:探索の結果を return で返せるか。返せるならループをメソッドかローカル関数に入れて return。返せず、今のメソッドのローカル変数を多く書き換えるなら、ループ直後のラベルへの前方 goto 1回か、各ループ条件で見る bool フラグ。
多重ループの抜け方の選び方。今回の計測では3つとも同じ速度だったので、決め手は読みやすさ。

目安はこうです。探索して結果を返す処理なら、ローカル関数(または別メソッド)+ return がいちばん読みやすく、テストもしやすい。ループが今のメソッドのローカル変数をいくつも書き換えていて切り出しにくいなら、ループ直後へ飛ぶ前方 goto 1回のほうがフラグより素直です。フラグは外側の条件に && !stop を書き忘れやすいので、最後の選択肢にしています。ローカル関数の書き方は C#のメソッドの記事で詳しく扱っています。

switch の goto case と goto default

C# の switch は case 間の暗黙のフォールスルーを許しません。case 1: の本体の最後に break がないと、それだけでエラーです。

error CS0163: Control cannot fall through from one case label ('case 1:') to another

別の case へ続けたいときは、goto case で明示的に書きます。料金プランごとに機能を積み上げて有効にする例です。

foreach (string plan in new[] { "pro", "plus", "free", "trial" })
{
    Console.Write($"{plan,-5}: ");
    switch (plan)
    {
        case "pro":
            Console.Write("priority-support ");
            goto case "plus";
        case "plus":
            Console.Write("no-ads ");
            goto case "free";
        case "free":
            Console.Write("basic");
            break;
        default:
            Console.Write("unknown -> ");
            goto case "free";
    }
    Console.WriteLine();
}
pro  : priority-support no-ads basic
plus : no-ads basic
free : basic
trial: unknown -> basic

case 5: がないのに goto case 5; と書くと error CS0159: No such label 'case 5:' within the scope of the goto statement です。case int n when n > 2: のようなパターンの case の中からでも、goto case 0; で定数の case へ移ることはできました(出力は big → zero-ish)。goto case のあとに書けるのは定数だけなので、複数の case で共有する処理は定数の case か default に置くのがよいでしょう。

まとめ

したいこと 使うもの
条件を満たしたらループを1つ終える break
今回の周回だけ飛ばす continue(while なら増分を上に)
多重ループを一度に終える ローカル関数 + return、または前方 goto
switch で別の case を続けて実行 goto case 値; / goto default;
入力が尽きるまで繰り返す while (true) + null チェック + break

while・for・foreach そのものの動きと、for と foreach の速度比較は 繰り返し文の記事にまとめました。ループの中で例外を捕まえて続けるか、そこで終えるかは 例外処理の記事を参考にしてください。

検証環境:.NET SDK 10.0.403 / ランタイム 10.0.14、C# 14(net10.0 の既定)、macOS、Apple Silicon Mac(M1 Pro)。

よくある質問

C#で多重ループを一度に抜けるには?

break はいちばん内側のループしか抜けません。ループをローカル関数やメソッドに入れて return する、ループ直後のラベルへ goto する、bool フラグを外側の条件に入れる、の3つがあります。2,000×2,000 の探索を100回繰り返した計測では3つとも約190〜200msで差がなかったので、読みやすいものを選べば十分です。

C#のswitchで次のcaseへ処理を続けるには?

C# は case 間の暗黙のフォールスルーを許さず、error CS0163 になります。次の case を実行したいときは goto case "値"; や goto default; を明示的に書きます。存在しない case へ goto すると error CS0159 です。

continueを使ったら無限ループになったのはなぜ?

while ループで i++ より前に continue があると、continue が増分を飛ばしてしまい、同じ値のまま回り続けます。for ループは continue のあとも反復式が実行されるので起きません。実際に while 版は i=2 で止まり、for 版は2だけ飛ばして 0,1,3,4 を出力しました。

  • #C#
  • #break
  • #continue
  • #goto
  • #繰り返し文

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑