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行でした。
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バイトでした。
目安はこうです。探索して結果を返す処理なら、ローカル関数(または別メソッド)+ 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 を出力しました。