C#における制御構文の一つである「goto文」は、プログラムの実行順序を強制的に指定したラベルの位置へ移動させる機能を持っています。
プログラミングの歴史において、goto文は「コードの可読性を損なう」「スパゲッティコードの原因になる」として忌避されてきた背景がありますが、C#というモダンな言語においても依然として言語仕様に含まれています。
それは、特定の状況下においてgoto文が最も効率的かつ簡潔にロジックを表現できる手段になり得るからです。
本記事では、goto文の基本的な使い方から、実務で役立つ具体的な活用例、そして使用を避けるべきケースまでを専門的な視点で詳しく解説します。
goto文の基本構文と仕組み
goto文は、プログラム内のあらかじめ定義された「ラベル」へ無条件にジャンプするための命令です。
C#においてラベルは、識別子の末尾にコロン : を付けることで定義します。
基本的な記述方法
goto文を使用する際は、まず移動先となる場所にラベルを配置し、その名称を goto キーワードの後に指定します。
using System;
public class Program
{
public static void Main()
{
Console.WriteLine("処理を開始します。");
// Startラベルへジャンプ
goto Start;
// この行はスキップされる
Console.WriteLine("このメッセージは表示されません。");
Start:
Console.WriteLine("Startラベルに到達しました。");
}
}
処理を開始します。
Startラベルに到達しました。
このように、goto Start; が実行されると、その間にあるコードはすべて無視され、プログラムの実行権が直ちに Start: ラベルの次の行へと移ります。
ラベルのスコープと制限
ラベルには一定のスコープ(有効範囲)が存在します。
C#においてラベルは、それが定義されたメソッド内でのみ有効です。
そのため、別のメソッド内にあるラベルへジャンプすることはできません。
また、変数の初期化を飛び越えてジャンプすることも制限されています。
例えば、ある変数が宣言される前にジャンプして、その変数を使用しようとするようなロジックはコンパイルエラーとなります。
switch文におけるgoto文の活用
C#の switch 文は、C++などの言語とは異なり、「フォールスルー(case間の暗黙的な移動)」が禁止されています。
つまり、ある case 節の処理が終わった後に、次の case 節へそのまま移行することはできず、必ず break や return などでブロックを抜けなければなりません。
しかし、共通の処理を行いたい場合や、特定の条件で別の case を実行したい場合には、goto case または goto default を使用することで、明示的に他のラベルへ制御を移すことが可能です。
switch文での実装例
以下のコードは、注文の段階に応じて処理を分岐させ、特定のステータスでは追加の処理を行うために goto を使用する例です。
using System;
public class SwitchExample
{
public static void Main()
{
int step = 1;
switch (step)
{
case 1:
Console.WriteLine("ステップ1:注文を受理しました。");
// ステップ2の処理へ明示的にジャンプ
goto case 2;
case 2:
Console.WriteLine("ステップ2:在庫を確認しています。");
break;
case 3:
Console.WriteLine("ステップ3:発送準備完了。");
break;
default:
Console.WriteLine("不明なステータスです。");
break;
}
}
}
ステップ1:注文を受理しました。
ステップ2:在庫を確認しています。
この例では、case 1 の処理が完了した後、goto case 2; によって case 2 のコードが実行されています。
これは、C#でフォールスルーに近い動作を安全かつ明示的に実現するための正当な手法の一つです。
多重ループからの脱出
goto文が最もその真価を発揮するのが、「深いネスト(入れ子)になったループ構造からの脱出」です。
通常の break 文では、現在実行されている最も内側のループ一つしか抜けることができません。
3重、4重のループから一度に抜け出すためには、フラグ変数を利用して各ループでチェックを行う必要がありますが、これではコードが冗長になりがちです。
goto文を使わない場合の多重ループ脱出
まず、goto文を使わずにフラグ管理で脱出する一般的な方法を見てみましょう。
bool found = false;
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
if (matrix[i, j] == targetValue)
{
found = true;
break; // 内側のループを抜ける
}
}
if (found)
{
break; // 外側のループを抜ける
}
}
この方法では、found というフラグの状態を各階層で確認しなければならず、ループが深くなるほど条件判定が複雑になります。
goto文を使った効率的な脱出
次に、goto文を用いた実装例を紹介します。
using System;
public class NestedLoopExample
{
public static void Main()
{
int[,] matrix = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
int targetValue = 5;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (matrix[i, j] == targetValue)
{
Console.WriteLine($"値 {targetValue} を位置 ({i}, {j}) で発見しました。");
// すべてのループを一気に抜ける
goto ExitLoop;
}
}
}
Console.WriteLine("値は見つかりませんでした。");
ExitLoop:
Console.WriteLine("ループ処理を終了しました。");
}
}
値 5 を位置 (1, 1) で発見しました。
ループ処理を終了しました。
このように goto ExitLoop; を使用することで、ループの階層に関わらず即座に目的の位置までジャンプできます。
コードのインデントが深くならず、ロジックの意図(「見つかったら即終了する」という意図)が非常に明確になります。
goto文を使用する際の制限事項
goto文は強力ですが、C#の言語仕様上、いくつかの厳格な制限が設けられています。
これらはプログラムの整合性を保ち、予期せぬ動作を防ぐためのガードレールとなります。
ブロック外からブロック内へのジャンプ
最も重要な制約は、「ブロック(中括弧 { } で囲まれた範囲)の外から中へ直接ジャンプすることはできない」という点です。
例えば、if 文の中や for ループの中に外部から突然飛び込むことは許可されていません。
// コンパイルエラーの例
goto InnerLabel;
if (condition)
{
InnerLabel: // ifブロックの中には外からジャンプできない
Console.WriteLine("Hello");
}
try-catch-finally構文における制限
例外処理を行う try-catch-finally ブロックにおいても、goto文には厳しいルールがあります。
- tryブロックの外から内部へジャンプすることはできない。
- finallyブロックの内部から外部へジャンプすることはできない。
- catchブロック内から外部へジャンプすることは可能だが、推奨されない。
特に finally ブロックは、リソースの解放などを確実に行うための場所であるため、そこから勝手に制御を飛ばすような行為は言語レベルで禁止されています。
goto文が「禁じ手」とされる理由とスパゲッティコード
goto文が歴史的に嫌われてきた最大の理由は、「スパゲッティコード」の生成を助長するためです。
スパゲッティコードとは、処理の流れ(コントロールフロー)が複雑に絡み合い、どこからどこへジャンプしているのかを人間が追跡しにくくなったプログラムを指します。
構造化プログラミングの欠如
現代のプログラミングは、関数(メソッド)やループ、条件分岐といった「構造化された単位」で考えることが基本です。
しかし、goto文を多用するとこの構造が破壊されます。
- 読み手の負担増: 上から下へ流れるというコードの基本原則が崩れ、読み手はラベルを探してコード内を上下に行き来しなければなりません。
- デバッグの困難さ: 特定の行に到達した際、どのルートを通ってそこに辿り着いたのかを把握することが難しくなります。
- リファクタリングの阻害: コードの一部を別のメソッドに切り出そうとした際、goto文が介在していると切り出しが困難になります。
そのため、goto文を使う場合は「本当に他の構造化構文(break, continue, returnなど)で代用できないか」を慎重に検討する必要があります。
構造化プログラミングとgoto文の使い分け
goto文を適切に扱うためには、他のジャンプステートメントとの役割の違いを理解しておくことが不可欠です。
他のステートメントとの比較
C#には、実行順序を制御するステートメントがいくつか存在します。
以下の表でその違いを確認しましょう。
| ステートメント | 主な用途 | 特徴 |
|---|---|---|
break | ループまたはswitchの終了 | 現在の階層のみを抜ける。 |
continue | 次のループ反復への移動 | ループの条件評価へスキップする。 |
return | メソッドの終了 | 呼び出し元へ戻り、値を返す。 |
goto | 任意のラベルへの移動 | メソッド内の自由な位置へジャンプ。 |
リファクタリングによる回避
多重ループの脱出であっても、メソッドを分割することで return 文を使い、goto文を避ける手法が推奨されることもあります。
// gotoを使わず、メソッド分割で対応する例
public bool TryFindValue(int[,] matrix, int target, out int row, out int col)
{
for (int i = 0; i < matrix.GetLength(0); i++)
{
for (int j = 0; j < matrix.GetLength(1); j++)
{
if (matrix[i, j] == target)
{
row = i;
col = j;
return true; // 見つかった時点でメソッド自体を終了
}
}
}
row = col = -1;
return false;
}
このように、ロジックを小さな単位に分割すれば、return だけでクリーンに多重ループを抜け出すことができます。
これが「構造化プログラミング」における標準的なアプローチです。
実務でgoto文を使用する際のガイドライン
どうしてもgoto文を使用する必要があると判断した場合には、以下のガイドラインを守ることでコードの品質低下を最小限に抑えることができます。
- 上から下へのジャンプのみに制限する
下から上へ戻るジャンプ(ループのような動作)に
gotoを使うのは避けるべきです。ループが必要な場合は
whileやforといった構造化された繰り返しを使って明示的に表現してください。- ジャンプ先を一つにまとめる
リソースのクリーンアップ処理やエラーハンドリングのために、共通の終了処理へ向かう用途だけで
gotoを使うのは効果的です(C言語的なエラーハンドリング)。複数の場所から同じ後処理へ移る場合に1つのラベルに集約すると読みやすくなります。
- ラベル名を明確にする
Label1のような意味のない名前は避け、ErrorExitやEndProcessのように目的がひと目で分かる名前を付けてください。ラベル名を明確にするとコードの意図が伝わりやすくなります。
- 多用しない
一つのメソッド内で複数の
gotoが飛び交う状況は、設計自体に問題があるサインです。必要以上に多用せず、関数分割や例外処理、構造化制御に置き換えて可読性と保守性を高めてください。
まとめ
C#における goto 文は、決して「使ってはいけない機能」ではありません。
特に switch文での明示的な移動 や、複雑な多重ループからの脱出 においては、他の構文よりもシンプルに記述できる場合があります。
しかし、その自由度の高さゆえに、安易な多用はプログラムの保守性を著しく低下させます。
まずは return によるメソッドの早期終了や、条件分岐の整理など、構造化された方法で解決できないかを検討してください。
その上で、どうしてもコードが複雑になりすぎる場合にのみ、慎重に goto 文を導入するのが、プロフェッショナルなエンジニアとしての賢明な判断と言えるでしょう。
適切な道具を適切な場面で使用することこそが、美しくメンテナンス性の高いC#プログラムを書くための鍵となります。






