switch文は何をしているのか? (C#)
switch 文(式)、使ってますか?
はじめに:switch は何をしているのか?if と何が違うのか
私たちが日常的に使う switch(C#では switch 文/式)は、見た目は分岐を整理してくれる「きれいな」構文ですが、内部では何が起きているのでしょうか。if と何が違うのか、そしてどのように最適化されているのかを、IL(Intermediate Language) を覗きながら確認してみます。
.NET の実行モデルと IL
C#(や F#、VB、C++/CLI など)のソースコードは、まず IL にコンパイルされ、その後 JIT(Just-In-Time) コンパイラや AOT(Ahead-Of-Time) コンパイラを通じて、最終的に各プラットフォームで実行可能なバイナリへと変換されます。
一度中間言語(IL)を挟むことで、CPU やプラットフォームの違いを吸収しやすくなり、最適化の余地も広がります。
IL を見てみよう(SharpLab)
switch の挙動を知るには、C#コードを IL へ変換し結果を可視化できるツール(SharpLab など)を使うのが手っ取り早いです。以下は、引数の int に応じて "0"〜"3" を返すシンプルな例です。
C#(概念例)
public string SampleSwitch(int arg)
{
switch (arg)
{
case 0: return "0";
case 1: return "1";
case 2: return "2";
case 3: return "3";
}
return "";
}
SharpLab で見ると


IL のポイント(要約)
- 引数を一度ローカル変数へ退避(
stloc.1→stloc.0など)し、評価 した値を使って分岐します。 - ジャンプ先アドレスを配列(テーブル)として定義 し、評価結果を インデックス としてジャンプ先を決定します。
- 「ジャンプ先を記録したテーブル(ジャンプテーブル)」があるため、配列アクセスは O(1)。単純な
ifの連鎖より効率的に動けるのが本質です。
この構造が、switch の「読みやすさ」以上の強みになっていることが分かります。
C#に置き直すとこんなイメージ
IL のジャンプテーブル構造を、C#での擬似コードに置き直すと以下のようなイメージになります。
public string SampleSwitch_(int arg)
{
// ローカル変数へ代入して評価
var localArg = arg;
// 「ジャンプ先」を関数配列で擬似表現
var funcs = new Func<string>[] {
() => "0",
() => "1",
() => "2",
() => "3",
};
if (0 <= localArg && localArg <= 3)
{
// 配列アクセス(O(1))で「返り値取得処理」に到達
return funcslocalArg;
}
return "";
}
実際の JIT 最適化はもっと賢いですが、概念的には「値 → インデックス → ジャンプ先」という流れです。
連番の整数に対する最適化:ジャンプテーブル
密な(連続した)整数キー に対する switch は、先ほどのような ジャンプテーブル による高速化が行われます。
このときの計算量は、配列アクセス+分岐 で非常に小さく、単純な if 連鎖より有利になります。
疎な整数や負の値を含む場合は?
では、キーが疎(例:1, 10, 100, 1000)だったり、負の値を含んだりする場合はどうでしょうか。

IL を確認すると、大小比較+等値比較 を組み合わせた分岐に展開されており、おおむね二分探索風 の形になります。C#での擬似表現は次の通りです。
public string SampleSwitch_(int arg)
{
var localArg = arg;
if (localArg > 10)
{
if (localArg == 100) return "100";
else if (localArg == 1000) return "1000";
}
if (localArg == 1) return "1";
if (localArg == 10) return "10";
return "";
}
この分岐は「およそ二分探索的」な形で、計算量は O(log n) に近い動きになります。疎なキー集合では、テーブル化よりも比較の組み合わせで効率を確保する、という方針が見て取れます。
文字列の switch はどうなる?

次に 文字列キー の switch を覗いてみると、IL では System.String::op_Equality を使った 等値比較の連鎖 へ素直に展開されていることが分かります。
C#のイメージ
public string SampleSwitch_(string arg)
{
var localArg = arg;
if (localArg == "one") return "1";
if (localArg == "two") return "2";
if (localArg == "three") return "3";
if (localArg == "four") return "4";
return "";
}
つまり、文字列 switch はほぼ単純な if 連鎖 と同様の形へ変換されます(肩書き・型パターンの switch も同様で、isinst 等による型チェックの直列化に近い構造です)。
「型」パターンの switch
型パターンの例では、IL 上で isinst による型判定が順に行われ、該当した型に応じて返り値をセットしています。これも、判定の直列化 に近い構造です。

学び:見た目はきれい、しかし中身は…
switch は構文としては読みやすく「きれい」ですが、実際の処理はケースによって泥臭い 最適化(あるいは単純な展開)になります。
それでも、密な整数キー のように、ジャンプテーブル が効く場面では強力です。逆に、大量のケース で最適化されないパターンを延々と回すと、当然ながら影響は出ます。
あえて言えば:別の手段を検討する余地
-
大量の文字列
switchを高頻度で回すなら、Dictionary<string, T>の利用を検討できます。TryGetValueは平均的に高速で、ケースの増減にも強いです。public string DictionarySwitch(string arg) { var table = new Dictionary<string, string>() { { "a", "A" }, { "b", "B" }, { "c", "C" }, // …いっぱい }; if (table.TryGetValue(arg, out var result)) { return result; } return ""; } -
型パターン
switchを多用するなら、インターフェース で責務を委譲したり、(純粋)仮想関数 へ切り出してディスパッチさせる設計も有効です。public interface IStringConvert { string Convert(string arg); } private IStringConvert _converter = new StringConvertA(); public string InterfaceSwitch(string arg) { return _converter.Convert(arg); }
これらは、コンパイラの展開/最適化に過度に依存しない ための選択肢として考えると楽しいはずです。
まとめ
-
switchは 密な整数キー では ジャンプテーブル により O(1) に近い効率で動作します。 - 疎な整数集合(負の値を含むなど)では、大小比較+等値比較を組み合わせた 二分探索的な形(O(log n)) に展開されます。
-
文字列の
switchは多くの場合、等値比較の連鎖 へ素直に展開されます。 - 見た目がきれいでも、内部の処理は必ずしも美しく最適化されるわけではありません。大量のケース を高頻度で回す場面では、
Dictionaryやインターフェース/仮想関数 への置き換えを検討すると良いです。
結局のところ、「switch は何をしているのか」を知っておくと、適材適所の選択 ができるようになります。場面に応じて、構文と実装(IL/JIT の展開)をイメージしながら使い分けていきたいところです。
Discussion
文字列のswitchでは数が多くなると最適化がかかります。
多くの場合はとありますが、もう少し増やすだけで最適化されますね
Sharplabで試したもの
参考