switch 式(C# 8+)は単なる switch 文の置き換えではなく、if-else 連鎖・ポリモーフィズム・Dictionary 分岐など従来の分岐ロジック全般を宣言的な「データから値への写像」として書き直す強力なツールです。特にビジネスルールの表現・HTTP ステータス分類・状態遷移・料金計算など「入力の分類に対して値を返す」場面で絶大な効果を発揮します。
本記事は10種類の実戦パターンをシナリオ別にコード例で示す実装レシピ集です。パターンマッチング全機能の包括的な解説はパターンマッチング完全ガイドにまとめているので、概念整理はそちらを参照してください。本記事は「どう書けば実務が楽になるか」に特化します。
- 10パターン早見表 — シナリオ別
- パターン① — if-else 連鎖をリファクタリング
- パターン② — HTTP ステータスコードの分類
- パターン③ — ロールベース認可
- パターン④ — 状態遷移マシン
- パターン⑤ — 判別共用体(DU)による階層処理
- パターン⑥ — コマンドハンドラーの振り分け
- パターン⑦ — 段階的料金計算(税率・手数料)
- パターン⑧ — エラーコードから例外へのマッピング
- パターン⑨ — null + 欠損値の統一処理
- パターン⑩ — switch 式のネストと分解
- switch 式 vs Dictionary vs virtual method
- switch 式をテストする
- よくある質問
- まとめ
10パターン早見表 — シナリオ別
| シナリオ | パターン | 使う機能 |
|---|---|---|
| if-else 連鎖の整理 | ① リファクタリング | 基本 switch 式 |
| HTTP ステータスの分類 | ② 範囲分岐 | 関係パターン(< >=) |
| ユーザー権限による分岐 | ③ ロールベース | enum + プロパティパターン |
| 状態遷移の制御 | ④ ステートマシン | タプルパターン |
| shape 階層の処理 | ⑤ 判別共用体(DU) | sealed record + 位置パターン |
| コマンドハンドラー振り分け | ⑥ コマンドディスパッチ | 型パターン |
| 段階的料金計算 | ⑦ 料金レンジ | 関係パターン連鎖 |
| エラーコード変換 | ⑧ enum → 例外 | シンプル値マッピング |
| null + 欠損値処理 | ⑨ null 安全分岐 | null パターン + when |
| switch式の組み合わせ | ⑩ ネスト分解 | 位置パターン + 入れ子 |
パターン① — if-else 連鎖をリファクタリング
// Before: if-else 連鎖(可読性が低い)
public string GetDiscountLabel(decimal amount, int loyaltyYears)
{
if (amount >= 100000)
{
return "プラチナ";
}
else if (amount >= 50000 && loyaltyYears >= 3)
{
return "ゴールド";
}
else if (amount >= 10000)
{
return "シルバー";
}
else
{
return "ブロンズ";
}
}
// After: switch式(条件が一目で見える・横に並ぶ)
public string GetDiscountLabel(decimal amount, int loyaltyYears) =>
(amount, loyaltyYears) switch
{
( >= 100000, _) => "プラチナ",
( >= 50000, >= 3) => "ゴールド",
( >= 10000, _) => "シルバー",
_ => "ブロンズ",
};
// リファクタリングの効果:
// - 行数が半分以下
// - 条件が縦に並ぶため「条件と結果」の対応が明確
// - 網羅性チェックでケース漏れを静的検出
// - 不変(式なので再代入できない)
パターン② — HTTP ステータスコードの分類
public enum HttpCategory
{
Informational, Success, Redirection,
ClientError, ServerError, Invalid
}
public static HttpCategory Classify(int statusCode) => statusCode switch
{
< 100 or >= 600 => HttpCategory.Invalid,
< 200 => HttpCategory.Informational,
< 300 => HttpCategory.Success,
< 400 => HttpCategory.Redirection,
< 500 => HttpCategory.ClientError,
_ => HttpCategory.ServerError,
};
// リトライ戦略: カテゴリから判断
public static bool ShouldRetry(int statusCode) => statusCode switch
{
408 or 429 or 503 or 504 => true, // 既知のリトライ可能
>= 500 and < 600 => true, // サーバーエラーは原則リトライ
_ => false,
};
// ユーザー表示メッセージ
public static string UserMessage(int statusCode) => statusCode switch
{
200 or 201 or 204 => "処理に成功しました",
400 => "リクエストに誤りがあります",
401 => "ログインが必要です",
403 => "この操作は許可されていません",
404 => "対象が見つかりません",
429 => "リクエストが多すぎます",
>= 500 => "サーバーエラーが発生しました",
_ => $"エラー: {statusCode}",
};
パターン③ — ロールベース認可
public enum Role { Guest, User, Admin, SuperAdmin }
public sealed record Principal(int UserId, Role Role, bool IsInternal);
public static bool CanDelete(Principal user, Document doc) => (user, doc) switch
{
// SuperAdmin は常に削除可能
({ Role: Role.SuperAdmin }, _) => true,
// Admin は内部ドキュメントのみ
({ Role: Role.Admin, IsInternal: true }, { Classification: "Internal" }) => true,
// 本人の下書きは削除可能
({ UserId: var uid }, { AuthorId: var aid, Status: "Draft" }) when uid == aid => true,
// その他は不可
_ => false,
};
// 細かい権限チェックもマッチさせられる
public static string? DenyReason(Principal user, Document doc) => (user, doc) switch
{
({ Role: Role.Guest }, _) => "ログインが必要です",
(_, { Status: "Archived" }) => "アーカイブ済み",
({ IsInternal: false }, { Classification: "Internal" })
=> "内部ドキュメントへのアクセス権なし",
_ => null, // 問題なし
};
パターン④ — 状態遷移マシン
public enum OrderStatus
{
Draft, Submitted, Paid, Shipped, Delivered, Cancelled, Refunded
}
public enum OrderAction
{
Submit, Pay, Ship, Deliver, Cancel, Refund
}
public static OrderStatus? NextStatus(OrderStatus current, OrderAction action) =>
(current, action) switch
{
(OrderStatus.Draft, OrderAction.Submit) => OrderStatus.Submitted,
(OrderStatus.Submitted, OrderAction.Pay) => OrderStatus.Paid,
(OrderStatus.Submitted, OrderAction.Cancel) => OrderStatus.Cancelled,
(OrderStatus.Paid, OrderAction.Ship) => OrderStatus.Shipped,
(OrderStatus.Paid, OrderAction.Refund) => OrderStatus.Refunded,
(OrderStatus.Shipped, OrderAction.Deliver)=> OrderStatus.Delivered,
_ => null, // 不正な遷移
};
// 使用側
OrderStatus status = OrderStatus.Paid;
OrderAction act = OrderAction.Ship;
OrderStatus? next = NextStatus(status, act);
if (next is null) throw new InvalidOperationException("この状態からこの操作はできません");
status = next.Value;
2変数以上の条件分岐では、
(state, action) switch { (A, X) => ..., (A, Y) => ..., ... } の形が決定表(decision table)のように縦に並び、「どの状態 + どの操作 で何が起きるか」がひと目で分かります。従来のネスト if や二次元 switch 文よりもビジネスルールの記述性が大幅に向上します。パターン⑤ — 判別共用体(DU)による階層処理
// 支払手段を判別共用体として表現
public abstract record Payment;
public sealed record CreditCard(string Number, int ExpiryYear) : Payment;
public sealed record BankTransfer(string Iban, string BankName) : Payment;
public sealed record ConvenienceStore(string StoreChain, string OrderId) : Payment;
public sealed record Cash : Payment;
// 決済手数料
public static decimal CalculateFee(Payment p, decimal amount) => p switch
{
CreditCard => amount * 0.03m, // 3%
BankTransfer(_, "UFJ") => 110m, // UFJ は固定
BankTransfer => 220m, // その他銀行
ConvenienceStore => 180m, // コンビニ決済
Cash => 0m, // 現金
_ => throw new NotSupportedException(),
};
// 決済の説明文生成
public static string DescribePayment(Payment p) => p switch
{
CreditCard(var num, var year) =>
$"クレジットカード ****{num[^4..]}({year}年有効)",
BankTransfer(var iban, var bank) =>
$"{bank} 銀行振込({iban})",
ConvenienceStore(var chain, var orderId) =>
$"{chain}(注文番号 {orderId})",
Cash => "現金",
_ => "不明",
};
// sealed record 階層なので新しい支払方法を追加し忘れると警告(CS8509)
パターン⑥ — コマンドハンドラーの振り分け
// コマンドを型で表現
public interface ICommand { }
public sealed record CreateUser(string Name, string Email) : ICommand;
public sealed record DeleteUser(int UserId) : ICommand;
public sealed record UpdateEmail(int UserId, string NewEmail) : ICommand;
public sealed class CommandBus(IUserService users, IEmailService emails)
{
public Task<Result> DispatchAsync(ICommand command, CancellationToken ct) =>
command switch
{
CreateUser(var name, var email)
=> users.CreateAsync(name, email, ct),
DeleteUser(var id)
=> users.DeleteAsync(id, ct),
UpdateEmail(var id, var newEmail)
when EmailValidator.IsValid(newEmail)
=> users.UpdateEmailAsync(id, newEmail, ct),
UpdateEmail _
=> Task.FromResult(Result.Fail("メールアドレスが不正")),
_ => throw new NotSupportedException($"未対応: {command.GetType().Name}"),
};
}
パターン⑦ — 段階的料金計算(税率・手数料)
// 購入金額ごとの手数料率
public static decimal CalculateFeeRate(decimal amount) => amount switch
{
< 1000 => 0.00m, // 少額は手数料なし
< 10000 => 0.05m, // 5%
< 100000 => 0.03m, // 3%
< 1000000 => 0.02m, // 2%
_ => 0.01m, // 1%
};
// 配送料の計算(重量 × 距離)
public static decimal ShippingFee(double weightKg, double distanceKm) =>
(weightKg, distanceKm) switch
{
( <= 1.0, <= 100) => 500m,
( <= 1.0, <= 1000) => 800m,
( <= 5.0, <= 100) => 1000m,
( <= 5.0, <= 1000) => 1500m,
( <= 20.0, _) => 3000m,
_ => throw new ArgumentException("対応外の重量"),
};
// 学年による料金(日本円)
public static int Tuition(int grade) => grade switch
{
1 or 2 or 3 => 30000, // 小学校低学年
4 or 5 or 6 => 40000, // 小学校高学年
7 or 8 or 9 => 50000, // 中学校
10 or 11 or 12 => 60000, // 高校
_ => throw new ArgumentOutOfRangeException(nameof(grade)),
};
パターン⑧ — エラーコードから例外へのマッピング
public enum ApiErrorCode
{
Unknown, InvalidRequest, Unauthorized, Forbidden,
NotFound, RateLimitExceeded, InternalError, ServiceUnavailable
}
public static Exception ToException(ApiErrorCode code, string message) => code switch
{
ApiErrorCode.InvalidRequest => new ArgumentException(message),
ApiErrorCode.Unauthorized => new UnauthorizedAccessException(message),
ApiErrorCode.Forbidden => new UnauthorizedAccessException(message),
ApiErrorCode.NotFound => new KeyNotFoundException(message),
ApiErrorCode.RateLimitExceeded => new InvalidOperationException($"Rate limit: {message}"),
ApiErrorCode.InternalError => new HttpRequestException(message),
ApiErrorCode.ServiceUnavailable => new HttpRequestException($"Unavailable: {message}"),
_ => new Exception($"Unknown: {message}"),
};
// 使用側
try { await CallApiAsync(); }
catch (ApiException ex)
{
throw ToException(ex.Code, ex.Message); // 標準例外型に変換
}
パターン⑨ — null + 欠損値の統一処理
public enum InputKind { Empty, Whitespace, Numeric, Email, Other }
public static InputKind ClassifyInput(string? input) => input switch
{
null or "" => InputKind.Empty,
{ Length: > 0 } s when s.Trim() == "" => InputKind.Whitespace,
{ Length: > 0 } s when long.TryParse(s, out _) => InputKind.Numeric,
{ Length: > 0 } s when s.Contains('@') => InputKind.Email,
_ => InputKind.Other,
};
// ユーザー情報の表示名決定
public static string DisplayName(User? user) => user switch
{
null => "ゲスト",
{ DisplayName: var d } when !string.IsNullOrWhiteSpace(d) => d,
{ FirstName: var f, LastName: var l }
when !string.IsNullOrWhiteSpace(f) && !string.IsNullOrWhiteSpace(l)
=> $"{l} {f}",
{ Email: var e } when !string.IsNullOrWhiteSpace(e) => e.Split('@')[0],
_ => "匿名ユーザー",
};
パターン⑩ — switch 式のネストと分解
// NG: 巨大な switch 式(100行超はアンチパターン)
public string Classify(Order o) => o switch
{
{ Status: "Draft", Items.Count: 0 } => "空の下書き",
{ Status: "Draft", Items.Count: > 0 } => "編集中",
{ Status: "Submitted", Payment: CreditCard } => "クレカ決済待ち",
{ Status: "Submitted", Payment: BankTransfer } => "銀行振込待ち",
// ... 50 ケース続く
};
// OK: 役割ごとに関数を分けて合成
public string Classify(Order o) => o.Status switch
{
"Draft" => ClassifyDraft(o),
"Submitted" => ClassifySubmitted(o),
"Paid" => ClassifyPaid(o),
_ => "不明",
};
private string ClassifyDraft(Order o) => o switch
{
{ Items.Count: 0 } => "空の下書き",
_ => "編集中",
};
private string ClassifySubmitted(Order o) => o.Payment switch
{
CreditCard => "クレカ決済待ち",
BankTransfer => "銀行振込待ち",
_ => "支払方法不明",
};
// 各メソッドが小さく、単体テストもしやすい
1 つの
switch 式が 20 ケースを超えると可読性が急激に落ちます。ケースをグループ化できるならプロパティごとに別メソッドに分ける、あるいは Dictionary ベースのルックアップテーブルに切り替えてください。「巨大な switch を1つのメソッドに書く」より「複数の小さな switch に分解する」方がレビュー時の認知負荷も単体テスト書きやすさも大幅に向上します。switch 式 vs Dictionary vs virtual method
| 手法 | 向いている場面 | 向いていない場面 |
|---|---|---|
switch 式 |
ビジネスルール・固定ケース数・条件が複雑 | 実行時に分岐を変えたい・数百ケース |
Dictionary ルックアップ |
キーが実行時に追加される・キーが多い | 条件が複雑・when 節が必要 |
| virtual method / polymorphism | 型階層が安定・オブジェクト指向設計 | 既存型に処理を追加したい(OCP 違反) |
// ① switch 式: 宣言的で分岐が一か所にまとまる
public static decimal Fee1(PaymentType t) => t switch
{
PaymentType.Credit => 0.03m,
PaymentType.Bank => 0.01m,
PaymentType.Cash => 0m,
_ => throw new NotSupportedException(),
};
// ② Dictionary: 実行時に登録できる・大量ケースに強い
private static readonly FrozenDictionary<PaymentType, decimal> _rates =
new Dictionary<PaymentType, decimal>
{
[PaymentType.Credit] = 0.03m,
[PaymentType.Bank] = 0.01m,
[PaymentType.Cash] = 0m,
}.ToFrozenDictionary();
public static decimal Fee2(PaymentType t) => _rates[t];
// ③ virtual method: 型ごとに挙動をカプセル化(OOP 的)
public abstract class PaymentMethod
{
public abstract decimal FeeRate { get; }
}
public class CreditPayment : PaymentMethod { public override decimal FeeRate => 0.03m; }
public class BankPayment : PaymentMethod { public override decimal FeeRate => 0.01m; }
public class CashPayment : PaymentMethod { public override decimal FeeRate => 0m; }
// 使い分けの基準:
// - 単純な値マッピング + 定数ケース数 → Dictionary
// - 条件分岐 + when 節が必要 → switch 式
// - 型に処理を追加したい・オブジェクト指向的 → virtual
switch 式をテストする
public class HttpCategoryTests
{
// [Theory] + [InlineData] で多数のケースを一気にテスト
[Theory]
[InlineData(-1, HttpCategory.Invalid)]
[InlineData(99, HttpCategory.Invalid)]
[InlineData(100, HttpCategory.Informational)]
[InlineData(200, HttpCategory.Success)]
[InlineData(301, HttpCategory.Redirection)]
[InlineData(404, HttpCategory.ClientError)]
[InlineData(500, HttpCategory.ServerError)]
[InlineData(600, HttpCategory.Invalid)]
public void Classify_MapsStatusCode(int code, HttpCategory expected)
{
Assert.Equal(expected, Classify(code));
}
// 境界値テスト: パターン変更時にリグレッションを検出
[Theory]
[InlineData(199, HttpCategory.Informational)]
[InlineData(200, HttpCategory.Success)] // 境界
[InlineData(299, HttpCategory.Success)]
[InlineData(300, HttpCategory.Redirection)] // 境界
public void Classify_BoundaryValues(int code, HttpCategory expected)
{
Assert.Equal(expected, Classify(code));
}
}
よくある質問
BenchmarkDotNet で実測してください。可読性を犠牲にして switch 式を採用する価値があるほどの差が出るケースは稀です。_ => throw new NotSupportedException(...) のように書いたスタックトレースは、switch 式を呼び出したメソッドを経由して記録されます。UnreachableException(.NET 7+)を使うとさらに意図が明確で、_ => throw new UnreachableException("Enum に新値が追加された") のように書くと「この分岐は絶対に来ないはず」という契約を表現できます。まとめ
| シナリオ | 推奨手法 |
|---|---|
| if-else 連鎖のリファクタ | タプルパターン + 関係パターン |
| HTTP/エラーコード分類 | 範囲(関係)パターン + 定数パターン |
| 権限チェック | プロパティパターン + when 節 |
| 状態遷移 | タプルパターン(決定表形式) |
| 判別共用体 | sealed record 階層 + 位置パターン |
| コマンドディスパッチ | 型パターン + record 分解 |
| 段階的料金 | 関係パターンの連鎖 |
| 例外変換 | enum → 型マッピング |
| null + 欠損値 | null パターン + when |
| 巨大 switch | メソッド分割・Dictionary 併用 |
全パターンの概念整理・C# バージョン別機能・List パターン等の詳細はパターンマッチング完全ガイド、例外ハンドリングとの組み合わせは例外フィルター(when句)完全ガイド、record による DU 設計はrecord 型完全ガイド、分岐構文全般の基本はif文・switch文完全ガイドを参照してください。

