C#で大量の条件分岐を書くとき、「if-else を並べるべきか、それとも switch を使うべきか」で迷うことは多いです。どちらが速いかだけでなく、enum・int・string・範囲条件といった違いで最適解が変わります。本記事では、コンパイラ最適化・データ構造・ベンチマークの観点から、実務で迷わないための判断軸を整理します。
C#のif-elseとswitch/caseはどちらが速いのか?結論から
最初に結論です。
- 「どちらが速いか」はコード内容・ケース数・データ型・実行環境に依存するため、一概に決めつけることはできません。
- 実務では、まずは読みやすさ・保守性を優先し、気になる箇所だけベンチマークで実測するのが現実解です。
- 多数の整数・enum・stringの分岐なら、多くの場合
switchが有利になりやすい傾向があります。 - 範囲条件や複合条件(
x < 0やa && bなど)は、switchよりif-elseの方が自然です。 - 「この集合に含まれているか」「値→処理のマッピング」は、HashSet / Dictionaryなどのデータ構造で置き換えられることが多く、こちらの方が高速かつ拡張しやすいです。
つまり、「if か switch か」という二択だけで悩まず、「分岐をそもそもデータ構造で置き換えられないか?」もセットで考えるのがポイントです。
コンパイラ最適化から見るif-elseとswitchの違い
int・enumの場合の挙動
int や enum を対象に switch を書くと、C#コンパイラやJITコンパイラは、ケース数や値のばらつきに応じて次のような最適化を行います。
- 値がほぼ連番(密)でケースが多い → ジャンプテーブルに近いコードに最適化
- 値がスカスカ(疎)・ケースが少ない → 比較と分岐(ifの束に近い)として展開
一方で if-else を素直に並べると、基本的には「上から順番に比較していく」形のコードになります。JITが多少並べ替える余地はありますが、switch のようにジャンプテーブル前提にはなりにくいです。
以下のようなコードを考えてみます。
int code = GetCode();
if (code == 0) DoA();
else if (code == 1) DoB();
else if (code == 2) DoC();
else if (code == 3) DoD();
else HandleDefault();
これを switch で書くとこうなります。
int code = GetCode();
switch (code)
{
case 0: DoA(); break;
case 1: DoB(); break;
case 2: DoC(); break;
case 3: DoD(); break;
default: HandleDefault(); break;
}
ケース数が増えるほど、switch の方がジャンプテーブル化されやすくなり、平均的には if-else よりも有利になることが多いです。ただし、ケースが2〜3個程度であれば体感できる差はまず出ません。
| 対象 | ケース数 | コンパイル後の典型挙動 | パフォーマンス傾向(目安) |
|---|---|---|---|
| int / enum | 〜3件程度 | どちらも比較+分岐 | 差はほぼゼロ。読みやすさ優先。 |
| int / enum | 4〜10件程度 | switchが比較列または簡易ジャンプテーブル | ややswitch有利になることが多い。 |
| int / enum | 10件以上 | switchがジャンプテーブル化されやすい | 規模が大きくなるほどswitch有利になりやすい。 |
ここで重要なのは「どう最適化されるかはコンパイラとJITの実装に依存する」という点です。最終的な答えは、実際にビルドして JIT されたコードを実行してみないと分かりません。
stringの場合のswitchの挙動
string に対する switch は、C#コンパイラが内部的に次のようなコードへ展開することが多いです。
- 対象文字列のハッシュ値を求める
- ハッシュ値ごとにグループ分けした条件分岐を行う
- 最後に
string.Equalsで実際に等価比較して確定する
これはざっくり言えば「ハッシュテーブル+等価比較」のようなイメージで、ケースの数が増えるほど if (s == "foo") ... else if (s == "bar") ... のようなコードより効率が良くなる場合が多くなります。
しかし、ここでもやはり最終的な性能は以下の要因に左右されます。
- ケース数(数件〜数十件〜数百件)
- キー同士の類似性(ハッシュ値の衝突状況)
- 実際の入力データでよくヒットするケース(分岐の偏り)
そのため、「string なら必ず switch が速い」とは言えず、「ケースが増えるほどswitchの方が有利になりやすい傾向がある」程度に捉えるのが安全です。
範囲条件・複合条件はif-elseが適任
switch が得意なのは「ある1つの値がどの候補と等しいか」を選ぶ場面です。対して、次のような処理は switch では表現しづらく、if-else の方が直感的です。
x < 0,0 <= x && x < 100のような範囲条件a && b,a || (b && c)のような複合条件- 複数の値の組み合わせ(例:
x > 0 && y < 0)
これらの条件を無理に switch に落とし込もうとすると、かえって可読性が落ち、バグの温床になります。パフォーマンス以前に、意図が明快な方を選ぶのが結果的に全体の品質を上げます。
ケース数と可読性から見た使い分けの目安
パフォーマンスと同じくらい重要なのが可読性です。分岐の規模に応じたおおまかな目安をまとめると、次のようになります。
| ケース数 | 推奨構造 | 理由 |
|---|---|---|
| 1〜3件 | if / 三項演算子 | 読みやすく、パフォーマンス差はほぼなし。 |
| 4〜10件 | switch またはif-else | どちらでもよいが、値ごとの分岐ならswitchが見通し良好。 |
| 10件以上 | switch か Dictionary 等にリファクタ | if-elseの連鎖は追いづらい。構造化されたswitchかデータ駆動を検討。 |
特に将来ケースが増えそうな箇所(プロトコルコード、ステータスコード、トークン種別など)は、最初から switch で書いておくと変更に強い構造になります。
分岐をデータ構造に置き換える:HashSetとDictionary
集合への所属判定はHashSetが最有力
「この値が禁止リストに含まれているか?」「このトークンは予約語か?」といった判定に、延々と if-else や switch を使っていないでしょうか。
こういった「集合への所属判定」は、ほとんどの場合 HashSet<T> に置き換えた方がシンプルで速くなります。
var blockedTokens = new HashSet<string>(StringComparer.Ordinal)
{
"if",
"else",
"switch",
"case"
};
if (blockedTokens.Contains(token))
{
// 予約語
HandleReserved(token);
}
else
{
// 通常識別子
HandleIdentifier(token);
}
ポイントは次の通りです。
- Contains の平均計算量は O(1)(ハッシュベースのため)
- 要素の追加・削除が簡単で、設定ファイルやDBからの読み込みにも向いている
- 分岐ロジックから「リストの中身」を切り離せるため、責務が明確になる
Dictionaryで「値→処理」をマッピングする
「コード値に応じて実行する処理が変わる」ような状況では、switch の代わりに Dictionary<TKey, Action> などを使って処理をデータとして管理する手もあります。
var handlers = new Dictionary<int, Action>
{
[0] = DoA,
[1] = DoB,
[2] = DoC
};
if (handlers.TryGetValue(code, out var handler))
{
handler();
}
else
{
HandleDefault();
}
戻り値が必要な場合は Func<TResult> や Func<T, TResult> を使います。
var calculators = new Dictionary<int, Func<int, int>>
{
[0] = x => x + 1,
[1] = x => x * 2,
[2] = x => x * x
};
int result = calculators.TryGetValue(code, out var calc)
? calc(value)
: value;
switch と Dictionary を比較したときの特徴は次のようになります。
| 項目 | switch | Dictionary |
|---|---|---|
| 可読性 | 小規模なうちは非常に読みやすい | 「データとしての表」にまとまるので見やすいことも多い |
| 拡張性 | コード修正が必要(ビルド必須) | 設定から読み込むなど、動的拡張も可能 |
| パフォーマンス | ジャンプテーブルなどで最適化されうる | ハッシュベース。ほぼ O(1) だがオーバーヘッドはやや大きめ |
| 用途 | コンパイル時に確定した分岐 | 外部設定やプラグインを含む柔軟な分岐 |
性能面での絶対優位はケースによりますが、「ビジネスルールが頻繁に変わる」ような場面では、Dictionary によるデータ駆動設計の方が結果的に安定した設計になることが多いです。
実際に測る:BenchmarkDotNetでifとswitchを比較する
最終的な答えは「自分の環境で実際に測る」です。そのための定番ライブラリが BenchmarkDotNet です。
BenchmarkDotNetの基本的な使い方(サンプル)
まず、プロジェクトに NuGet から BenchmarkDotNet を追加し、次のようなベンチマーククラスを作成します。
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
public class BranchBenchmarks
{
private readonly int[] _data;
public BranchBenchmarks()
{
var random = new Random(0);
_data = Enumerable.Range(0, 1000)
.Select(_ => random.Next(0, 10))
.ToArray();
}
[Benchmark]
public int IfElse()
{
int sum = 0;
foreach (var x in _data)
{
if (x == 0) sum += 1;
else if (x == 1) sum += 2;
else if (x == 2) sum += 3;
else if (x == 3) sum += 4;
else if (x == 4) sum += 5;
else if (x == 5) sum += 6;
else if (x == 6) sum += 7;
else if (x == 7) sum += 8;
else if (x == 8) sum += 9;
else sum += 10;
}
return sum;
}
[Benchmark]
public int Switch()
{
int sum = 0;
foreach (var x in _data)
{
switch (x)
{
case 0: sum += 1; break;
case 1: sum += 2; break;
case 2: sum += 3; break;
case 3: sum += 4; break;
case 4: sum += 5; break;
case 5: sum += 6; break;
case 6: sum += 7; break;
case 7: sum += 8; break;
case 8: sum += 9; break;
default: sum += 10; break;
}
}
return sum;
}
}
public class Program
{
public static void Main(string[] args)
{
BenchmarkRunner.Run<BranchBenchmarks>();
}
}
このようにして、IfElse と Switch を同じ条件で比較します。実務ではこれに加えて、
HashSetを使うバージョンDictionaryを使うバージョン- 文字列版(
string)
なども並べて計測すると、「自分のコードでは何が最適か」が見えてきます。
ベンチマークでのよくある落とし穴
ベンチマークを書くときに注意したいポイントも押さえておきましょう。
- Debug ビルドで測ってしまう → 必ず Release ビルド+最適化有効で測る。
- 対象コードが短すぎる・ループ回数が少なすぎる → ノイズが大きく、測定誤差に埋もれる。BenchmarkDotNetは自動で繰り返してくれるが、ループ回数も意識する。
- GCの影響を無視する → 途中でたまたまGCが走ってしまうと結果がぶれる。BenchmarkDotNetの
[MemoryDiagnoser]属性でGCの発生状況も確認するとよい。 - 分岐の偏りを考慮していない → 実際の本番データでよくヒットするケースを再現しないと、現実のパフォーマンスとは異なる結果になる。
つまり、ベンチマークは「本番に近い条件をどれだけ正確に再現できるか」が重要です。synthetic な小さなサンプルだけを見て「if より switch が常に速い!」といった一般論を導くのは危険です。
modern C#の観点:switch式とパターンマッチング
C# 7以降では、switch にパターンマッチング機能が追加され、さらに C# 8 以降では switch式 も導入されました。これらは主にコードの表現力と可読性を高めるための機能です。
例えば、従来の switch を次のような switch 式に書き換えられます。
string GetLabel(int code) => code switch
{
0 => "Zero",
1 => "One",
2 => "Two",
_ => "Other"
};
また、型や条件によるパターンマッチングも行えます。
string Describe(object value) => value switch
{
null => "null",
int i when i < 0 => "negative int",
int i => "non-negative int",
string s => $"string({s.Length})",
_ => "other"
};
これらはコンパイル後には結局 switch + 条件分岐などに展開されます。パフォーマンス的にはif-elseと大差がないことも多く、むしろ「複雑な条件を安全に・読みやすく書ける」ことに価値があります。
特に enum に対する switch 式では、全ての列挙値を列挙しないとコンパイラ警告が出るため、「抜け漏れ」に気付きやすいという利点もあります。
パフォーマンス以外で考えるべきポイント
保守性と変更容易性
多くの場合、長期的な開発コストを決めるのは「数%の速度差」ではなく、
- 新しいケースを追加するときにバグを埋め込みにくいか
- 仕様書やドキュメントと行単位で対応づけやすいか
- チームメンバーがすぐ意図を読み取れるか
といった観点です。
- 固定的なコード値→処理 →
switchで列挙すると、仕様書と見比べやすい。 - 外部の設定やプラグインで動的に変わる処理 →
Dictionaryや DI(依存性注入)を使ってデータ駆動にした方が自然。
エラー検知のしやすさ
switch を使うと、default を書き忘れたときや、enum の新しい値を追加したときに警告が出るケースがあります。これにより、「新しいケースを追加したのに処理を実装し忘れた」という事故を防ぎやすくなります。
if-else は柔軟な代わりに、どこに分岐があるのかをコンパイラが完全には追いきれないため、変更漏れをコンパイル時に検出しづらいという側面があります。
よくある誤解・アンチパターン
誤解1:switchは常にifより高速である
よくある誤解が「switchはジャンプテーブルになるから常に高速」というものです。実際には、
- ケースが少ないとき
- 値がスカスカで連番ではないとき
- コンパイラやJITのバージョンによる違い
などにより、switch が if より遅くなる可能性もあります。あくまで傾向として、「多数のケースではswitchが有利になりやすい」程度に理解しておくとよいでしょう。
誤解2:文字列のswitchは必ず遅い
「文字列比較は高コストだから string の switch は避けるべき」と言われることもありますが、内部的にはハッシュベースの分岐になっている実装が多く、単純な if-else の列より効率的に動くことも少なくありません。
もちろん、文字列生成や ToLower() / ToUpper() 呼び出しなどを毎回行っていると、それだけでコストが積み上がります。例えば次のような書き方は避けるべきです。
// 悪い例:毎回ToLowerを呼び出している
switch (command.ToLower())
{
case "start":
Start();
break;
case "stop":
Stop();
break;
}
StringComparer.OrdinalIgnoreCase などのケースインセンシティブな比較を提供するコンパレータを利用する、あるいは一度だけ正規化してから使う方が良い場合が多いです。
誤解3:分岐を書き直すだけで劇的に速くなる
パフォーマンス問題の多くは、
- 不要なメモリアロケーション(
newの多用、string連結など) - I/O 待ち(DBアクセス、ファイル読み込み、ネットワーク)
- 不必要に複雑なアルゴリズム(O(n^2) のループなど)
に起因しており、「if を switch に変えた」程度では体感できる改善が得られないことがほとんどです。条件分岐の微最適化はあくまで最後の一押しであり、まずは大局的なボトルネックを特定することが重要です。
実務での判断フロー:どう選べばよいか
ここまでの内容を踏まえて、実務での判断フローを整理してみます。
| 状況 | 推奨アプローチ | 理由 |
|---|---|---|
| 分岐が数件だけ | if / 三項演算子 | シンプルで読みやすく、性能差はほぼなし。 |
| int / enum で10件以上の分岐 | switch | ジャンプテーブル化が期待でき、構造も明快。 |
| 文字列キーが多数 | switch または Dictionary | switchはハッシュベースの分岐になりやすく、Dictionaryはデータ駆動設計に向く。 |
| 「この集合に含まれているか?」 | HashSet<T>.Contains | O(1) で高速、要素の追加削除も簡単。 |
| 範囲条件・複合条件 | if-else または switch式+パターンマッチング | switch の本領は等価比較。条件式はifの方が自然。 |
| 外部仕様に合わせて頻繁に追加・変更される分岐 | Dictionary+設定ファイルなど | ビルドなしで差し替えやすく、ルールをデータとして管理しやすい。 |
| パフォーマンスがシビアなホットパス | 候補を絞ってBenchmarkDotNetで実測 | コンパイラ・JIT・CPUの違いを含めて、本当に速いものを選べる。 |
サンプル:配列・リストに対する分岐の書き方
最後に、質問のモチーフである「配列・リストの値に対して複数の分岐を行う」ケースをいくつかのパターンで比較してみます。
パターン1:単純なコード値に応じて処理を分ける
典型的には switch が分かりやすく、パフォーマンス面でも有利になりやすいパターンです。
foreach (var item in items)
{
switch (item.Code)
{
case 0: HandleA(item); break;
case 1: HandleB(item); break;
case 2: HandleC(item); break;
default: HandleDefault(item); break;
}
}
パターン2:禁止コードのチェックだけをしたい
この場合は HashSet<int> にするとスッキリします。
var bannedCodes = new HashSet<int> { 3, 5, 7, 9 };
foreach (var item in items)
{
if (bannedCodes.Contains(item.Code))
{
Reject(item);
}
else
{
Accept(item);
}
}
パターン3:コードに応じて異なる処理オブジェクトを呼び出す
この場合は Dictionary<int, IHandler> のような構成も有力です。
public interface IItemHandler
{
void Handle(Item item);
}
var handlers = new Dictionary<int, IItemHandler>
{
[0] = new HandleA(),
[1] = new HandleB(),
[2] = new HandleC()
};
foreach (var item in items)
{
if (handlers.TryGetValue(item.Code, out var handler))
{
handler.Handle(item);
}
else
{
DefaultHandler.Instance.Handle(item);
}
}
こうしておくと、新しいコード値と処理を追加したいときにDictionaryへのエントリ追加だけで済むため、巨大な switch 文をいじるよりも安全なことが多いです。
まとめ:if/elseとswitchの「速度」より大事なこと
ここまでの内容を整理すると、次のようにまとめられます。
- 最終的な速度はコードと実行環境に依存するため、「if より switch の方が常に速い」とは言えない。
- 整数・enum の多数ケースでは、switch がジャンプテーブル化されて有利になることが多い。
- string の多数ケースでは、switch が内部的にハッシュベースの分岐になるため、有利になりやすい。
- 範囲条件・複合条件は if-else の方が自然で、switch で無理に書く必要はない。
- 集合への所属判定は HashSet<T>、コード値→処理のマッピングは Dictionary<TKey, TDelegate> にすると設計がシンプルになる。
- modern C# の switch 式とパターンマッチングは、パフォーマンスよりも表現力と保守性向上が主目的。
- 本当にシビアな箇所は、BenchmarkDotNet などで実測した結果をもとに判断する。
日常の開発では、まずは「読みやすくバグを生みにくい書き方」を選び、必要になった時点で「if / switch / HashSet / Dictionary」など複数案をベンチマークしてみる、というスタンスが現実的です。
「if と switch、どちらが速いか?」という問いをきっかけに、分岐そのものの設計や、データ駆動へのリファクタリングまで含めて見直してみると、アプリケーション全体の品質向上にもつながるはずです。

コメント