C# switch徹底解説:基本構文からswitch式・パターンまで

この記事ではC#の条件分岐(if/switch)を体系的に解説し、switch-caseの書き方(int/文字列/enum、default、break)やフォールスルー禁止、ケースガード、C#8.0のswitch式・パターンマッチングまで整理。読みやすい分岐の選び方と実装の迷いを解決します。

目次

C#のswitchとは:条件分岐の基本と用途

csharp+switch+conditional

C#のswitchは、ある1つの値(式)の内容に応じて処理を分岐させるための条件分岐構文です。複数の候補(case)から一致するものを選ぶ、という発想で書けるため、同じ変数を何度も比較するような場面で可読性が上がりやすいのが特徴です。

典型的には「状態」「種別」「メニュー選択」「入力コマンド」など、値の候補がある程度決まっているケースで活躍します。if文でも同じことはできますが、分岐の意図(どの値で分けるのか)がswitchのほうが明確になりやすく、レビューや保守で理解されやすい傾向があります。

if文との違いと使い分けの判断基準

if文は「条件式がtrueかどうか」で分岐するのに対し、C# switchは「式の値がどれに一致するか(またはパターンに合うか)」で分岐するのが基本です。この違いにより、向いている場面が変わります。

使い分けの判断基準は、次の観点で整理すると実務で迷いにくくなります。

  • 同じ対象を何度も比較しているか:同一の変数(例:statusやtype)に対して「==」が連続するならswitchのほうが読みやすくなりがちです。
  • 分岐の候補が離散的で固定か:候補が「A/B/C…」のように明確で、増減も管理しやすいならswitchが適します。
  • 範囲判定・複合条件が多いか:大小比較(>、<)や論理演算(&&、||)を多用する条件はifのほうが直感的です。
  • 読み手に“分類”を伝えたいか:「この値はどのカテゴリか」を示したい場合、switchの形がそのまま分類表のように機能します。

まとめると、「同じ値を基準に、複数の候補へ分岐する」ならc# switchが有利で、「条件式そのものが複雑・連続的(範囲や複数条件)」ならif文が扱いやすい、というのが基本方針になります。

switchで扱える型と前提条件(数値・文字列・列挙体など)

C# switchは、分岐の基準となる式(switchの対象)が特定の型である必要があります。実務で頻出するのは、数値・文字列・列挙体(enum)です。これらは「候補が有限で、caseとして列挙しやすい」ため、switchと相性が良い代表例です。

主に扱える型・使われやすい型は次のとおりです。

  • 数値型:intなどの整数を中心に、数値の離散値での分岐に使われます。
  • 文字列(string):コマンド名や種別コードなど、定義済みの文字列で分岐する用途で使われます。
  • 列挙体(enum):状態や種別を型として表現でき、caseの網羅性も意識しやすいため、switchの王道です。

また、switchで安全・明快に分岐するための前提条件として、次の点を押さえておくと設計が安定します。

  • 分岐対象は「候補が定義されている」こと:場当たり的な値(自由入力の無限パターン)より、仕様として候補が決まっている値に向きます。
  • caseに書く値は“判定の基準”として明確であること:マジックナンバーや曖昧な文字列を乱立させると、switchの利点(読みやすさ)が失われます。
  • 比較の意味がブレないこと:文字列での分岐は、同義語や表記ゆれが混ざると想定外の分岐漏れが起きやすくなります。値の定義を揃えることが重要です。

このように、c# switchは「扱える型」と「値の性質(候補が明確であること)」が噛み合うほど効果を発揮します。まずは数値・文字列・列挙体のような“候補が決まる入力”から導入すると、コードの意図が伝わりやすくなります。

switch文の基本構文(case/break/default)

csharp+switch+patternmatching

C#のswitch文(c# switch)は、1つの式の値に応じて処理を分岐させるための構文です。基本形は「switch(判定したい値)→ caseで分岐 → 必要に応じてdefaultで補完」という流れで、各caseの末尾では処理の区切りを明示します。まずは最小構成を押さえると、読みやすく安全な分岐を組み立てられます。

switch (value)
{
    case 1:
        // value が 1 のときの処理
        break;

    case 2:
        // value が 2 のときの処理
        break;

    default:
        // どの case にも一致しないときの処理
        break;
}

caseラベルの書き方と注意点

caseラベルには「switch式の値と比較できる定数(またはそれに準ずる値)」を指定します。読みやすさの観点では、caseの粒度を揃え、意味のある値(例:enumや定数)を使うほど意図が明確になります。

  • caseは上から順に評価され、最初に一致したブロックが選ばれます。

  • 重複するcase値は書けません(同じ値のcaseを複数置くとコンパイルエラーになります)。

  • caseに書けるのは基本的に「定数として扱える値」です。変数や計算結果を直接caseに置くのは避け、必要なら事前に値を確定させる設計にします。

int(整数)で分岐する例

整数はswitchの代表的な用途です。例えばメニュー番号やステータスコードなど、「数値=意味」が決まっている場合に向きます。数値リテラルをそのまま書くと意図が伝わりにくいので、用途によっては定数化(const)も検討すると可読性が上がります。

int statusCode = 200;

switch (statusCode)
{
    case 200:
        Console.WriteLine("OK");
        break;

    case 404:
        Console.WriteLine("Not Found");
        break;

    case 500:
        Console.WriteLine("Server Error");
        break;

    default:
        Console.WriteLine("Unknown Status");
        break;
}

注意点として、caseの値を増やすほど「意味の説明」が不足しがちです。コメントを補うか、可能であればenumへ寄せると、分岐の意図がコード上で自己説明的になります。

string(文字列)で分岐する例

文字列でのswitchは、コマンド名や種別キーなど、入力が「特定の文字列集合」に限られるときに使えます。ただし、文字列比較は入力揺れ(大文字小文字・空白・全角半角など)に弱いため、switch前に正規化してから分岐すると堅牢です。

string command = "start";

// 例:前処理で小文字化して揺れを減らす
command = command.Trim().ToLowerInvariant();

switch (command)
{
    case "start":
        Console.WriteLine("開始します");
        break;

    case "stop":
        Console.WriteLine("停止します");
        break;

    case "status":
        Console.WriteLine("状態を表示します");
        break;

    default:
        Console.WriteLine("不明なコマンドです");
        break;
}

設計のコツとして、caseに書く文字列が増えてきたら「許容する入力の一覧」が散らばりやすくなります。文字列のまま続けるか、enumや定数へ寄せるかを早めに判断すると保守が楽になります。

enum(列挙体)で分岐する例

enum(列挙体)でのswitchは、分岐条件を「取り得る値の集合」として型で表現できるため、意図が明確で安全性も高い書き方です。業務状態(State)や種別(Type)など、固定の選択肢で分岐する場合に特に有効です。

enum OrderStatus
{
    Pending,
    Paid,
    Shipped,
    Cancelled
}

OrderStatus status = OrderStatus.Paid;

switch (status)
{
    case OrderStatus.Pending:
        Console.WriteLine("未決済です");
        break;

    case OrderStatus.Paid:
        Console.WriteLine("決済済みです");
        break;

    case OrderStatus.Shipped:
        Console.WriteLine("発送済みです");
        break;

    case OrderStatus.Cancelled:
        Console.WriteLine("キャンセルされました");
        break;

    default:
        Console.WriteLine("想定外の状態です");
        break;
}

enumを使うとcaseの意味が読み取りやすくなり、「数値や文字列のマジック値」を減らせます。結果として、c# switchの分岐が仕様に沿っているかレビューもしやすくなります。

default節の役割と設計のコツ

default節は「どのcaseにも一致しなかったときの受け皿」です。入力のバリエーションが増えたり、想定外の値が紛れたりする状況でも、処理を安全に収束させる役割があります。

  • ユーザー入力・外部データを扱う場合は、defaultでエラーメッセージ表示やログ出力など、運用上追跡できる形にしておくと原因究明が容易です。

  • 想定外を見逃したくない場合は、defaultで例外を投げるなど「異常として扱う」設計も有効です(ただし、ユーザー体験や呼び出し側の例外処理方針と整合させます)。

  • defaultは必須ではありませんが、仕様上「必ずどれかに一致する」前提が崩れたときの挙動が不明瞭になりがちです。保守性の観点から、意図を持って付ける/付けないを決めるのがコツです。

switch (statusCode)
{
    case 200:
        Console.WriteLine("OK");
        break;

    default:
        // 想定外の値を明示的に扱う
        Console.WriteLine($"Unexpected status: {statusCode}");
        break;
}

breakの意味と制御フロー

breakは、そのcaseの処理を終えた後にswitch文全体を抜けるための制御文です。c# switchでは、各caseの末尾で処理の区切りをはっきりさせることが重要で、breakは「ここで分岐処理を完結させる」という意図をコードに表します。

  • breakがあると:該当caseを実行後、switch文の外へ移動します。

  • breakがないと:caseブロックの終端に到達した時点でコンパイルエラーになりやすく、意図しない制御フローを防ぐ仕組みになっています(「必ずどこかで抜ける」ことを要求されるイメージです)。

  • case内でswitchを抜ける方法はbreakだけでなく、returnでメソッド自体を終了させるなど、設計に応じた制御フローも取り得ます。ただし、読みやすさの観点では「基本はbreakで終える」形を揃えると理解しやすくなります。

switch (command)
{
    case "start":
        Console.WriteLine("開始");
        break; // switchを抜ける

    case "stop":
        Console.WriteLine("停止");
        break;

    default:
        Console.WriteLine("不明");
        break;
}

// ここに制御が戻ってくる

フォールスルー(fall-through)とC#における制約

csharp+switch+programming

C#でフォールスルーが原則禁止される理由

他言語のswitchでは、breakを書き忘れると次のcaseへ処理が流れ込む「フォールスルー(fall-through)」が起きることがあります。一方、c# switch(switch文)では、原則としてフォールスルーが禁止されており、各caseは明示的に制御を終了(または移動)させる必要があります。

この制約がある主な理由は、次のとおりです。

  • バグの温床になりやすい:意図せず次のcaseを実行してしまうのは、典型的な「書き忘れバグ」です。C#はこれを言語仕様で防ぎます。
  • 読み手の認知負荷を下げる:C#では「そのcaseに入ったら、そこで処理が完結する」ことが基本で、コードの意図が追いやすくなります。
  • 制御フローを明確化する:各caseの末尾が、breakreturnthrowなどの“ジャンプ”で閉じられるため、到達不能や意図しない実行経路を減らせます。

実際、C#では次のような「次のcaseにそのまま落ちる」書き方はコンパイルエラーになります。

switch (x)
{
    case 1:
        Console.WriteLine("one");
        // break がないので次へフォールスルー…させたいが、C#では原則NG
    case 2:
        Console.WriteLine("two");
        break;
}

つまり、c# switchは「break忘れによる事故」を設計段階で起こしにくくする思想を持っています。その代わり、複数のcaseを同じ処理にまとめたい場合は、C#が許可する方法で明示的に表現します。

意図的に同じ処理へまとめる代表的な書き方(複数caseの統合)

フォールスルーが禁止されていても、実務では「複数の値を同じ扱いにする」ケースがよくあります。c# switchでは、意図的に処理をまとめる代表的な書き方として、次のパターンがよく使われます。

1) caseラベルを連続して並べ、最後のcaseに処理を書く

空のcaseを並べ、最後のcaseでまとめて処理する方法です。これはC#で公式に認められている「安全な統合」で、読み手にも意図が伝わりやすい書き方です。

switch (statusCode)
{
    case 200:
    case 201:
    case 204:
        Console.WriteLine("成功扱い");
        break;

    case 400:
    case 404:
        Console.WriteLine("クライアントエラー扱い");
        break;

    default:
        Console.WriteLine("その他");
        break;
}

この形では、case 200:case 201:に処理が書かれていないため、「意図してまとめている」ことが明確です。結果として、フォールスルー禁止の思想を保ったまま、複数条件の統合ができます。

2) goto caseで“次のcaseの処理を使い回す”

別のcaseで定義した処理へ明示的に移動したい場合、C#ではgoto caseが使えます。これは「暗黙のフォールスルー」ではなく、明示的なジャンプとして読み取れる点が重要です。

switch (command)
{
    case "start":
        Console.WriteLine("開始処理の前準備");
        goto case "run"; // run の処理を続けて実行する

    case "run":
        Console.WriteLine("実行処理");
        break;

    default:
        Console.WriteLine("不明なコマンド");
        break;
}

goto caseは便利ですが、乱用すると制御フローが追いにくくなるため、「共通化の意図が明確で、分岐が浅い」場面に絞るのが無難です。

3) goto defaultで既定処理に寄せる

特定の入力を「defaultと同じ扱い」にしたいときはgoto defaultで統合できます。これも意図が明示されます。

switch (input)
{
    case null:
        goto default;

    case "":
        goto default;

    default:
        Console.WriteLine("入力が空、または未指定");
        break;
}

このように、c# switchではフォールスルーを原則禁止にする代わりに、「複数caseの統合」を安全かつ明示的に書ける手段が用意されています。結果として、意図しない分岐ミスを避けつつ、分岐ロジックの重複も抑えられます。

より柔軟なswitch:ガード条件とパターンマッチング

csharp+switch+patternmatching

C#のswitchは「値が一致したら分岐する」だけでなく、条件を追加したり、型や構造に基づいて分岐したりできます。特にパターンマッチングの導入以降、c# switchは複雑な判定ロジックを読みやすく整理する手段として強力になりました。このセクションでは、whenガード、型による分岐、nullの扱い、複数条件のまとめ方に絞って解説します。

whenガードで条件を追加する(ケースガード)

whenガード(ケースガード)は、caseの一致に加えて追加条件を課したいときに使います。「値が特定範囲なら同じcaseとして扱いたい」「同じ値でも状況によって分岐を変えたい」といった要件で、ifをネストせずに書けるのが利点です。

int score = 85;

switch (score)
{
    case int s when s >= 90:
        Console.WriteLine("A");
        break;

    case int s when s >= 80:
        Console.WriteLine("B");
        break;

    case int s when s >= 70:
        Console.WriteLine("C");
        break;

    default:
        Console.WriteLine("D");
        break;
}

ポイントは次のとおりです。

  • case int s when 条件のように、when以降に真偽条件を書ける
  • 上から順に評価されるため、範囲判定は「より厳しい条件(上位)」を先に置く
  • whenの中では、入力値を束縛した変数(例:s)を使って条件を記述できる

型による分岐(型スイッチ/isパターン)

c# switchは、対象がobjectなどの抽象的な型でも、実体の型に応じて分岐できます。これにより、キャストの連鎖やis判定の多段ifを、switchで整理できます。

object value = GetValue();

switch (value)
{
    case int i:
        Console.WriteLine($"int: {i}");
        break;

    case string s:
        Console.WriteLine($"string: {s}");
        break;

    case DateTime dt:
        Console.WriteLine($"DateTime: {dt:yyyy-MM-dd}");
        break;

    default:
        Console.WriteLine("unknown type");
        break;
}

型スイッチの実務上のメリットは、次の2点です。

  • case 型 変数名で「型チェック+変数への取り出し」を同時にできる(安全なキャストが前提になる)
  • 型ごとの処理がまとまり、追加・修正時に影響範囲を追いやすい

さらに、型判定に加えて条件も付けたい場合はwhenと組み合わせられます。

switch (value)
{
    case string s when s.Length == 0:
        Console.WriteLine("empty string");
        break;

    case string s:
        Console.WriteLine($"string: {s}");
        break;
}

nullチェックと変数スコープの注意点

パターンマッチングでは、nullの扱いと「束縛した変数のスコープ」を理解しておくことが重要です。ここを曖昧にすると、意図しない分岐やコンパイルエラーにつながります。

nullの扱いは、明示的にcase null:を用意しておくと意図が伝わりやすくなります。

object value = null;

switch (value)
{
    case null:
        Console.WriteLine("value is null");
        break;

    case string s:
        Console.WriteLine($"string: {s}");
        break;

    default:
        Console.WriteLine("other");
        break;
}

変数スコープについては、case string s:のように束縛した変数は、そのcaseブロック内で使うのが基本です。また、同一switch内で同名のパターン変数を繰り返し使うと、スコープの重なり方によってはコンパイルエラーになることがあります(caseラベルは同一のswitchブロック内に並ぶため、同名を安易に使い回さないのが安全です)。

実務では次の方針が堅実です。

  • 束縛変数名(例:s, i)はcaseごとに重複しない命名にする、またはcaseごとにブロック({ ... })を明確に切る
  • nullの可能性がある入力では、case nullを先に書いて意図を固定する

複数の値・条件をまとめて扱う方法

似た処理を複数の入力で共有したいとき、c# switchでは「複数caseの列挙」や「whenで条件をまとめる」といった方法で、重複コードを減らせます。

複数の値を同じ処理にまとめる場合は、caseを並べて同一処理へ落とし込めます。

string command = "start";

switch (command)
{
    case "start":
    case "run":
    case "begin":
        Console.WriteLine("execute");
        break;

    case "stop":
    case "end":
        Console.WriteLine("terminate");
        break;

    default:
        Console.WriteLine("unknown");
        break;
}

複数条件をまとめて扱うなら、whenで論理演算子を使って集約できます。

int x = 12;

switch (x)
{
    case int n when n >= 1 && n <= 10:
        Console.WriteLine("1-10");
        break;

    case int n when n == 11 || n == 12 || n == 13:
        Console.WriteLine("11-13");
        break;

    default:
        Console.WriteLine("other");
        break;
}

まとめ方の選択基準は次のとおりです。

  • 「完全一致の列挙」なら複数caseを並べるほうが読みやすい
  • 「範囲・複合条件」ならwhenで表現したほうが意図が明確になりやすい

C# 8以降のswitch式(switch expression)の使いどころ

csharp+switch+patternmatching

C# switchを「値を返す分岐」として書きたい場面では、C# 8以降で導入されたswitch式(switch expression)が強力です。switch文が“処理の分岐”に向くのに対し、switch式は“式として評価される”ため、変数への代入や戻り値の決定を簡潔に表現できます。分岐結果を1つの値として扱えるので、条件分岐が散らばりにくく、意図(この入力ならこの出力)も読み取りやすくなります。

switch式の基本構文(=> と式ベースの分岐)

switch式は「入力(式)」に対して、各アーム(分岐)で「結果(式)」を返す構文です。最大の特徴は、caseやbreakではなく => を使って「このパターンならこの値」を表す点にあります。構造としては、入力 switch { パターン => 結果, ... } となり、全体が1つの式として評価されます。

string GetStatusText(int code)
{
    return code switch
    {
        200 => "OK",
        404 => "Not Found",
        500 => "Server Error",
        _   => "Unknown" // 最後はフォールバック(ワイルドカード)
    };
}

上記のように、C# switch式は「戻り値を決める」用途と相性が良く、メソッドのreturnをそのまま分岐にできます。また、ローカル変数への代入にも適しています。

var category = score switch
{
    >= 80 => "A",
    >= 60 => "B",
    _     => "C"
};

switch式では各アームの右辺が“式”であることが重要です。単文の処理ではなく「値を返す」設計に寄せることで、コードの見通しが良くなります。もし複数行の処理が必要なら、結果として返す値を事前に計算する(別メソッド化する等)ことを検討すると、switch式のメリットを損ないにくくなります。

後置き(式としての)記法の読み方

switch式は、従来のswitch文と違い「対象(入力)」が前にあり、後ろに switch { ... } が続く“後置き”の形になります。慣れないうちは、次の順番で読むと理解しやすいです。

  • まず左側の「入力」を読む(何を判定しているか)
  • 次に switch ブロックの各アームを上から順に読む(どのパターンで、何を返すか)
  • 最後に _(どれにも当てはまらない場合)を確認する

たとえば次のコードは「入力がorderStateのとき、状態に応じた表示名を返す」と読めます。

var label = orderState switch
{
    OrderState.Draft     => "下書き",
    OrderState.Confirmed => "確定",
    OrderState.Shipped   => "出荷済み",
    _ => "不明"
};

ポイントは、switch式全体が値を返すため、label の決定ロジックが1か所にまとまることです。C# switchの分岐が「代入」「return」に散りがちな場合、後置き記法は“結果が何か”をコード上で明確にしやすくなります。

switch文とswitch式の選択基準

C# switchを使うとき、switch文とswitch式のどちらを選ぶかは「分岐の目的」で判断するとブレにくいです。switch式は万能ではなく、得意な形に寄せるほど効果が出ます。

  • 分岐の結果を1つの値として返したい(return・代入・初期化)ならswitch式が向く
  • 各ケースで返す値が明確で、右辺を式として書けるならswitch式が向く
  • 各ケースで複数の手続き(ログ、更新、I/Oなど)を行うなど、処理の塊を分岐したいならswitch文が向く
  • 結果を返すのではなくフロー制御が主目的(途中でbreak/continue相当の制御をしたい等)ならswitch文が無難

まとめると、switch式は「値を決めるための条件分岐」を簡潔に書くのが得意です。C# switchを“式として使う”ことで、条件分岐の意図を「この入力ならこの出力」という形に寄せられ、保守時の読みやすさにもつながります。

switchが肥大化したときの代替設計(ディスパッチの考え方)

csharp+switch+dispatch

C#のswitchは読みやすい条件分岐として便利ですが、caseが増え続けると「どこで何をしているか」が見えにくくなり、修正時の影響範囲も広がります。こうした肥大化は、分岐そのものが悪いのではなく、分岐の中身(処理)が密結合になっていることが原因になりがちです。

このとき有効なのが「ディスパッチ(dispatch)」の考え方です。分岐条件から“実行すべき処理”を引き当て、処理本体は別のメソッド(またはデリゲート)に委譲します。結果として、switchは「ルーティング(振り分け)」に集中し、個々の処理は独立して見通しよく保てます。

分岐ロジックをメソッドへ分割して見通しを良くする

最もシンプルな改善は、case内の処理を専用メソッドへ切り出すことです。C# switchが持つ「分岐の可読性」は維持しつつ、各caseの“業務処理”を短くできます。特に、case内で変数を何度も更新したり、例外処理やログ処理が混ざっている場合は、メソッド分割の効果が大きくなります。

ポイントは、switch側に残すのは「どのメソッドを呼ぶか」だけにし、処理の詳細はメソッドへ閉じ込めることです。

public void Handle(string command)
{
    switch (command)
    {
        case "Create":
            HandleCreate();
            break;

        case "Update":
            HandleUpdate();
            break;

        case "Delete":
            HandleDelete();
            break;

        default:
            HandleUnknown(command);
            break;
    }
}

private void HandleCreate()
{
    // Create処理(詳細はここへ)
}

private void HandleUpdate()
{
    // Update処理(詳細はここへ)
}

private void HandleDelete()
{
    // Delete処理(詳細はここへ)
}

private void HandleUnknown(string command)
{
    // 未対応コマンドの扱い
}

この形の利点は、caseの追加・修正が「メソッド追加」とセットで行えるため、変更単位が小さくなる点です。また、各メソッドを個別にテストしやすくなり、switchの肥大化による保守性低下を抑えられます。

デリゲートで分岐先を割り当てる(メソッドディスパッチ)

caseがさらに増える場合、switch自体を“ディスパッチテーブル”に置き換える発想が有効です。具体的には、キー(コマンドや種別)と実行する処理(デリゲート)をDictionaryに登録し、入力から処理を引き当てて実行します。これにより、分岐の追加は「テーブルへ登録するだけ」になり、巨大なswitchを編集する頻度を減らせます。

private readonly Dictionary<string, Action> _dispatch;

public MyHandler()
{
    _dispatch = new Dictionary<string, Action>(StringComparer.OrdinalIgnoreCase)
    {
        ["Create"] = HandleCreate,
        ["Update"] = HandleUpdate,
        ["Delete"] = HandleDelete
    };
}

public void Handle(string command)
{
    if (_dispatch.TryGetValue(command, out var action))
    {
        action();
        return;
    }

    HandleUnknown(command);
}

この方法は、C# switchを「書き換え続ける」運用から、「処理の対応表を更新する」運用へ移行できるのが強みです。一方で、登録漏れがあると実行時に未対応扱いになるため、初期化時の登録を一箇所に集約し、意図が伝わる命名(dispatch、handlers等)にするのがコツです。

Action/Funcで簡潔に書くパターン

ディスパッチでは、戻り値が不要ならAction、結果を返したいならFuncを使うと記述が簡潔になります。特に「入力 → 判定 → 戻り値」という形の分岐は、switchを長くしがちなので、Funcで引き当てて呼び出す形にすると読みやすくなります。

戻り値を返す例(Func):

private readonly Dictionary<string, Func<int>> _dispatch;

public MyCalculator()
{
    _dispatch = new Dictionary<string, Func<int>>
    {
        ["A"] = CalcA,
        ["B"] = CalcB
    };
}

public int Calculate(string mode)
{
    if (_dispatch.TryGetValue(mode, out var func))
        return func();

    return CalcDefault();
}

private int CalcA() => 10;
private int CalcB() => 20;
private int CalcDefault() => 0;

引数が必要なら、Action<T>やFunc<T, TResult>にすれば同じ考え方で拡張できます。C# switchのcaseで複数の値を加工してから返すような処理でも、「加工ロジックを関数として分離する」ことで見通しが良くなり、分岐の責務(選ぶだけ)を保てます。

staticメソッドを含む構成での実装ポイント

ディスパッチ先をstaticメソッドにする構成は、状態を持たない処理(純粋な変換、フォーマット、単純な計算など)で特に相性が良いです。インスタンスに依存しないため、テストや再利用がしやすく、登録も明快になります。

一方で、static化する際は「状態(フィールド)に触らない」ことが前提になります。もしコンテキスト(引数や外部依存)が必要なら、引数として渡す形に寄せると破綻しにくくなります。

private static readonly Dictionary<string, Action<RequestContext>> _dispatch
    = new(StringComparer.OrdinalIgnoreCase)
{
    ["Create"] = HandleCreate,
    ["Update"] = HandleUpdate
};

public void Handle(string command, RequestContext ctx)
{
    if (_dispatch.TryGetValue(command, out var action))
    {
        action(ctx);
        return;
    }

    HandleUnknown(ctx, command);
}

private static void HandleCreate(RequestContext ctx)
{
    // ctxを通じて必要情報を受け取る(staticなのでフィールドに頼らない)
}

private static void HandleUpdate(RequestContext ctx)
{
}

private static void HandleUnknown(RequestContext ctx, string command)
{
}

また、staticディスパッチテーブルを使う場合は、初期化タイミングが明確になるようにreadonlyで固定し、登録内容を実行中に変更しない設計にすると安全です。これにより、switch肥大化の代替としての「安定した分岐基盤」として運用しやすくなります。

switchと三項演算子(条件演算子)の使い分け

csharp+switch+ternary

C#では条件分岐の書き方として、switchと三項演算子(条件演算子)?:のどちらもよく使われます。ただし両者は「得意な状況」が違い、無理に置き換えると可読性や保守性が落ちることがあります。ここでは、c# switchと条件演算子をどう使い分けるべきかを、判断基準と具体例で整理します。

条件演算子が適するケース/避けたいケース

条件演算子は「短い条件で、値を返す」ことに特化した書き方です。一方で、条件や分岐先が増えると急速に読みにくくなるため、適用範囲を見極めるのが重要です。

条件演算子が適するケースは、次のような状況です。

  • 2択(真/偽)の分岐である(分岐が増えない)
  • 返したい値が1つで、式として書きたい(代入や引数にそのまま渡したい)
  • 各分岐の内容が短く副作用がない(メソッド呼び出しが多段にならない)

たとえば、表示用の文言を2択で決める程度なら、条件演算子は非常に読みやすくなります。

bool isAdmin = GetIsAdmin();
string label = isAdmin ? "管理者" : "一般ユーザー";

このように「1行で意図が伝わる」場合は、c# switchより条件演算子のほうが簡潔で、式として扱える点もメリットです。

一方で、条件演算子を避けたいケースも明確です。

  • 分岐が3つ以上になり、ネスト(入れ子)が発生する
  • 条件式が複雑で、どの条件でどの値になるのか追いづらい
  • 戻り値の型が揺れてしまい、読み手が最終的な型を推測しづらい
  • 分岐ごとに処理が長く、副作用(ログ、更新、例外など)が混ざる

典型的な「避けたい例」は、条件演算子のネストです。動作は書けてしまいますが、読む側の負担が大きくなります。

// 3択以上を条件演算子のネストで書くと読みづらい
string statusText =
    status == 0 ? "未処理" :
    status == 1 ? "処理中" :
    status == 2 ? "完了" :
    "不明";

このように分岐が増えるなら、c# switchで「値ごとの対応表」として表現したほうが、意図がはっきりします。条件演算子は「短い2択」、switchは「複数の候補から選ぶ」という役割分担を意識すると、コードの見通しが崩れにくくなります。

よくある落とし穴とベストプラクティス

csharp+switch+patternmatching

C#のswitchは見通しよく条件分岐を書ける一方で、ケース漏れや意図しない挙動、可読性の低下といった「ありがちな落とし穴」も存在します。ここでは c# switch を安全に運用するために、網羅性・制御フロー・規約面のベストプラクティスを整理します。

caseの網羅性(漏れ防止)とdefaultの扱い

switchで最も多い不具合は「想定外の値が来たときの挙動が定義されていない」ことです。caseの網羅性を高め、defaultの役割を明確にするだけで、バグの混入率は大きく下がります。

まず意識したいのは、defaultを“とりあえず”置かないことです。defaultがあると一見安全に見えますが、実は新しい値が追加されたとき(例:列挙体に要素が追加されたとき)に「気付けない」コードになりがちです。

  • defaultは「到達したら不正」という方針を明確にできる場合に有効(例外やログで検知する)
  • defaultが“何となく無視”や“何となく成功”を返すと、障害が潜伏しやすい

例えば列挙体の分岐では、想定外を握りつぶさず検知できるようにします。

public enum Status
{
    New,
    InProgress,
    Done
}

public string ToLabel(Status status)
{
    switch (status)
    {
        case Status.New:
            return "新規";
        case Status.InProgress:
            return "進行中";
        case Status.Done:
            return "完了";
        default:
            throw new ArgumentOutOfRangeException(nameof(status), status, "未対応のStatusです");
    }
}

一方、stringや数値など「入力が外部由来で不正値が来うる」ケースでは、defaultは安全側に倒すための受け皿として有用です。ただし、その場合も「何が起きたか」を追えるように、ログやエラー値の返却方針を決めておくと運用が安定します。

例外・戻り値・到達不能コードの整理

c# switch の保守性を下げる要因として、caseごとに「returnがあったりなかったり」「例外を投げたり投げなかったり」して制御フローが散らかる問題があります。読み手が“最後にどこへ到達するのか”を追いにくくなり、修正時に事故が起きやすくなります。

ベストプラクティスは、各caseの終了方法を揃えることです。典型は次の2パターンです。

  • 各caseでreturnして完結させ、switchの外に処理を残さない
  • 各caseで値を確定させ、最後に1回だけreturnする

例えば「各caseでreturnする」書き方に統一すると、break忘れや後続処理の巻き込みを避けられます。

public int GetPriority(string level)
{
    switch (level)
    {
        case "low":
            return 1;
        case "middle":
            return 5;
        case "high":
            return 10;
        default:
            throw new ArgumentException("未対応のlevelです", nameof(level));
    }
}

また、defaultや特定caseで例外を投げた後に、意味のない処理が残っていると「到達不能コード」になったり、将来の変更で到達してしまう危険があります。switchの後段に処理を残す場合は、switch内で必ず値が確定する構造にしておくと安全です。

public string Normalize(string input)
{
    string result;

    switch (input)
    {
        case null:
            throw new ArgumentNullException(nameof(input));
        case "":
            result = "(empty)";
            break;
        default:
            result = input.Trim();
            break;
    }

    return result;
}

ポイントは、例外を投げる分岐(ここではcase null)と、値を確定させる分岐(それ以外)を混在させても、「戻り値が必ず代入される」ことがコードから明確な形にすることです。

可読性を保つためのコーディング規約のポイント

switchはケースが増えるほど肥大化しやすく、読みづらくなるとレビュー・改修コストが跳ね上がります。規約として“読みやすさの基準”を決めておくと、チーム開発でも品質を維持しやすくなります。

  • case内は短く:処理が3〜5行を超えるなら、意図が分かるメソッド名に切り出して見通しを確保する
  • インデントと空行のルールを統一:caseの塊ごとに空行を入れる/入れない、return派かbreak派かを混在させない
  • マジック文字列・数値を避ける:caseラベルが文字列だらけになる場合は、定数化して表記ゆれを防ぐ
  • defaultの方針を規約化:例外で落とすのか、エラー値を返すのか、ログを残すのかをプロダクトの要件に合わせて統一する
  • コメントは「なぜ」に絞る:処理内容の説明より、仕様上の理由・例外的な条件を短く残す

特に「return派/break派の混在」は、読み手がswitchの出口を追う負担が増えます。プロジェクト内でどちらかに寄せ、どうしても例外が必要な場合のみ理由をコメントで補足すると、c# switchの可読性とレビュー効率が安定します。

(参考)仕様・公式ドキュメントで確認すべき観点

csharp+switch+patternmatching

C#のswitchは、バージョン更新に伴い「パターンマッチング」や「switch式」などの機能が拡張されてきました。そのため、現場で「c# switch」の挙動に迷ったときは、経験則だけで判断せず、言語仕様や公式ドキュメントで“どのルールに従って評価されるのか”を確認するのが安全です。

特に、コンパイル時に弾かれるケース(到達不能、網羅性不足、パターンの重複など)と、実行時に分岐結果が変わるケース(null、型階層、ガード条件など)は混同しやすいため、仕様に立ち返る観点を持つことが重要です。

switch/パターンマッチングの仕様確認ポイント

switch文・パターンマッチングは「どのcaseが一致するか」を厳密な順序とルールで決めています。以下は、公式ドキュメント(Microsoft LearnのC#言語リファレンス)や、必要に応じてC#言語仕様で確認しておくと、バグや誤解を減らせる代表的なポイントです。

  • 使用しているC#言語バージョンで利用可能な構文か

    プロジェクト設定(LangVersion)やターゲットフレームワークにより、同じ「c# switch」でも使える構文・警告・エラーが変わります。switchのパターンマッチング関連はバージョン差が出やすいので、まず「その書き方がその環境で正式にサポートされるか」を確認します。

  • caseの評価順序(上から順にマッチする)

    パターンマッチングを含むswitchでは、基本的に上から順にパターンが評価され、最初に一致したcaseが選ばれます。より一般的なパターン(例:var、_、基底型の型パターン)を上に置くと、後続のより具体的なcaseが到達不能になることがあるため、仕様上の「順序依存」を意識して設計します。

  • 到達不能(unreachable)・冗長(redundant)と判断される条件

    switchのパターンが前のcaseに包含される場合、コンパイラが到達不能としてエラー/警告にすることがあります。例えば、先に「常に真になり得るパターン」を置くと、後続が成立しません。どの程度までコンパイラが静的に判定するかは、言語仕様と実装(コンパイラの解析能力)に依存するため、想定外の警告が出たら公式の説明に当てるのが確実です。

  • nullの扱い(nullパターン、nullable参照型の文脈)

    参照型やnullable型をswitchに渡す場合、nullが入力になり得ます。nullがcaseに一致する条件、nullチェックがどの段階で行われるか、nullable参照型(NRT)の警告がどう出るかは、仕様・ドキュメント上の重要ポイントです。特に「nullを別caseで扱う」「nullを許容しない前提で書く」いずれの方針でも、言語がどう解釈するかの確認が必要です。

  • 型パターンのマッチ条件(型変換の有無、派生型の一致)

    型による分岐(型パターン)は「実行時の型」が一致するかで判定されます。どの型が一致と見なされるか、インターフェースや継承階層での挙動、ボックス化(objectに入った値型)など、境界条件での期待値は公式説明に従って整理します。

  • ガード条件(when)の評価タイミング

    パターンが一致したうえでwhen条件が評価されるのか、あるいは条件式の副作用が設計上問題にならないか、といった点は確認対象です。パターン一致→when判定→次のcaseへ、という流れを前提にすると読みやすい一方、例外が起き得る式や重い処理をwhenに書く設計は避けるべきケースがあります(どこまでを仕様として担保するかを理解するためにも公式記述の確認が有効です)。

  • 変数のスコープと代入(パターン変数がどこから有効か)

    パターンで導入した変数(例:型パターンで取り出す変数)は、どの範囲で確実に代入済みと見なされるかが決まっています。case節内だけで有効なのか、when内で参照できるのか、複数case間で名前が衝突したらどうなるのか、といったスコープ規則は、誤解するとコンパイルエラーや意図しない参照につながるため、仕様で裏取りしておくと安全です。

  • パターンの網羅性(exhaustiveness)と最終到達点

    分岐が「すべての入力をカバーしている」とコンパイラが判断できるかどうかは、型(特にenumやnullable)やパターンの書き方に依存します。網羅性が評価される条件は言語のルールに基づくため、default相当の扱いが必要か、例外を投げるべきか、到達不能の整理をどうするかを検討する際に公式の定義が役立ちます。

上記の観点を押さえたうえで、一次情報としてはMicrosoft LearnのC#リファレンス(switch、パターンマッチング)を確認し、より厳密な根拠が必要な場合はC#言語仕様に当たるのが確実です。公式記述と手元のコンパイラ挙動が異なるように見える場合は、対象のC#バージョン・コンパイラ実装・警告レベルの違いが原因になりやすい点も合わせて確認してください。