【C#】switch式の実戦パターン集|if-elseリファクタ・Strategy/State Machine・HTTP/認可/DDDまで10パターン

【C#】switch式でのパターンマッチング活用(型・プロパティ・論理パターン) C#

switch 式(C# 8+)は単なる switch 文の置き換えではなく、if-else 連鎖・ポリモーフィズム・Dictionary 分岐など従来の分岐ロジック全般を宣言的な「データから値への写像」として書き直す強力なツールです。特にビジネスルールの表現・HTTP ステータス分類・状態遷移・料金計算など「入力の分類に対して値を返す」場面で絶大な効果を発揮します。

本記事は10種類の実戦パターンをシナリオ別にコード例で示す実装レシピ集です。パターンマッチング全機能の包括的な解説はパターンマッチング完全ガイドにまとめているので、概念整理はそちらを参照してください。本記事は「どう書けば実務が楽になるか」に特化します。

スポンサーリンク

10パターン早見表 — シナリオ別

シナリオ パターン 使う機能
if-else 連鎖の整理 ① リファクタリング 基本 switch 式
HTTP ステータスの分類 ② 範囲分岐 関係パターン(< >=)
ユーザー権限による分岐 ③ ロールベース enum + プロパティパターン
状態遷移の制御 ④ ステートマシン タプルパターン
shape 階層の処理 ⑤ 判別共用体(DU) sealed record + 位置パターン
コマンドハンドラー振り分け ⑥ コマンドディスパッチ 型パターン
段階的料金計算 ⑦ 料金レンジ 関係パターン連鎖
エラーコード変換 ⑧ enum → 例外 シンプル値マッピング
null + 欠損値処理 ⑨ null 安全分岐 null パターン + when
switch式の組み合わせ ⑩ ネスト分解 位置パターン + 入れ子

パターン① — if-else 連鎖をリファクタリング

Before / After
// 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 ステータスコードの分類

ステータス 3桁を意味のあるカテゴリに変換
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}",
};

パターン③ — ロールベース認可

プロパティパターン + when 節
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)による階層処理

sealed record 階層 + 位置パターンで Visitor 的処理
// 支払手段を判別共用体として表現
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)

パターン⑥ — コマンドハンドラーの振り分け

型パターンで CQRS 的なコマンド処理
// コマンドを型で表現
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)),
};

パターン⑧ — エラーコードから例外へのマッピング

外部 API のエラーコードを型付き例外に変換
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 + 欠損値の統一処理

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 => "銀行振込待ち",
    _            => "支払方法不明",
};

// 各メソッドが小さく、単体テストもしやすい
switch 式が 20 行を超えたら分割を検討
1 つの switch 式が 20 ケースを超えると可読性が急激に落ちます。ケースをグループ化できるならプロパティごとに別メソッドに分ける、あるいは Dictionary ベースのルックアップテーブルに切り替えてください。「巨大な switch を1つのメソッドに書く」より「複数の小さな switch に分解する」方がレビュー時の認知負荷も単体テスト書きやすさも大幅に向上します。

switch 式 vs Dictionary vs virtual method

手法 向いている場面 向いていない場面
switch ビジネスルール・固定ケース数・条件が複雑 実行時に分岐を変えたい・数百ケース
Dictionary ルックアップ キーが実行時に追加される・キーが多い 条件が複雑・when 節が必要
virtual method / polymorphism 型階層が安定・オブジェクト指向設計 既存型に処理を追加したい(OCP 違反)
同じ処理を3つの方法で書いた比較
// ① 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 式をテストする

各ケースをカバーする xUnit テスト
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));
    }
}

よくある質問

Qswitch 式と switch 文のどちらを使うべきですか?
A値を返す・式として扱える処理なら switch 式、複数文の実行や副作用を伴う処理なら switch 文です。現代の C# では 8 割以上のケースで switch 式が適切で、「switch 式で書けないか」を先に検討する習慣をつけると、副作用の少ない宣言的なコードに自然となります。
Qswitch 式と if-else はどう使い分けますか?
A「入力の分類」で分岐が決まるなら switch 式、「独立した複数条件」を順番に評価するなら if-else が適切です。switch 式は「1つの対象をパターンにマッチさせる」ので、複数の異なる変数を順番にチェックするような処理(例: フォームバリデーションの連鎖)では if-else の方がわかりやすくなります。
Q巨大な switch 式は悪いコードですか?
Aケース数そのものよりも「凝集度」が重要です。HTTP ステータスのように「全ケースが意味的にまとまっている」なら数十ケースでも問題ありません。逆に「1つの switch 式の中で異なる種類の分岐が混在している」場合は責務の分離を疑うべきです。分割の基準として「20 ケース」や「メソッド本体の50% を超える」を超えたらリファクタリングを検討してください。
Qswitch 式のパフォーマンスは if-else より速いですか?
A多くの場合で同等か少し速い程度です。コンパイラは整数・enum・文字列の定数マッチに対してジャンプテーブルやハッシュベースの最適化を行います。ただしパフォーマンスが重要なホットパスで違いを出したければ、BenchmarkDotNet で実測してください。可読性を犠牲にして switch 式を採用する価値があるほどの差が出るケースは稀です。
Qswitch 式で例外を投げるとスタックトレースは綺麗に出ますか?
A正常に出ます。_ => 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文完全ガイドを参照してください。