C#における条件分岐は、プログラムの論理構造を決定する極めて重要な要素です。

その中でも switch文 は、特定の変数や式の値に基づいて処理を分岐させる際に、if-else文よりも高い視認性と保守性を提供します。

かつてのC#におけるswitch文は、単純な整数の比較や文字列の完全一致に限定されていましたが、近年のアップデートによってその機能は劇的に進化しました。

現在のC#では、単なる値の比較にとどまらず、型判定やプロパティの検証、さらには複雑な条件式を組み合わせた パターンマッチング が中心的な役割を担っています。

また、従来の「文 (Statement)」としての形式だけでなく、値を返す「式 (Expression)」としての記述も可能になり、より簡潔でバグの混入しにくいコードを書くことができるようになりました。

本記事では、基本的なswitch文の書き方から、最新のC#で利用可能な高度なパターンマッチングの使い方まで、実例を交えて詳細に解説します。

C#におけるswitch文の基本構造

C#のswitch文は、指定した式の値に応じて実行するコードブロックを選択する制御構造です。

まずは、最も伝統的な形式である「switch文」の基本をおさらいしましょう。

基本的な構文とキーワード

switch文は、switch キーワードに続く括弧内に判定対象となる式を記述します。

その後の波括弧内に、値ごとの処理を case ラベルとして定義します。

C#
using System;

class Program
{
    static void Main()
    {
        string weather = "Cloudy";

        // 基本的なswitch文
        switch (weather)
        {
            case "Sunny":
                Console.WriteLine("今日は晴れです。外出に最適です。");
                break;
            case "Cloudy":
                Console.WriteLine("今日は曇りです。折りたたみ傘があると安心です。");
                break;
            case "Rainy":
                Console.WriteLine("今日は雨です。傘を忘れずに。");
                break;
            default:
                // どのcaseにも一致しない場合の処理
                Console.WriteLine("天候情報が不明です。");
                break;
        }
    }
}
実行結果
今日は曇りです。折りたたみ傘があると安心です。

switch文における重要なルール

C#のswitch文には、他の言語(C++など)とは異なる厳格なルールが存在します。

break文の必須化

C#では、空でないcaseラベルにおいて次のcaseへ処理を継続させるフォールスルーは禁止されています。

各caseの末尾には必ず breakreturnthrow などのジャンプ文を記述する必要があります。

defaultラベル

いずれのcaseにも該当しない場合に実行される処理を指定するラベルです。

必須ではありませんが、予期しない値に対処するために記述することが推奨されます。

複数ラベルの共有

処理内容が同じである場合に限り、複数のcaseラベルを並べて記述し、一つの処理ブロックを共有することが可能です。

共有した処理ブロックの末尾にも必ず break 等のジャンプ文を置いて制御を明示してください。

C#
switch (errorCode)
{
    case 400:
    case 401:
    case 403:
        Console.WriteLine("クライアントエラーが発生しました。");
        break;
    default:
        Console.WriteLine("その他のエラーです。");
        break;
}

switch文からswitch式へ:モダンな記述方法

C# 8.0以降、従来のswitch文をより洗練させた switch式 が導入されました。

これは「文」ではなく「式」として扱われるため、結果をそのまま変数に代入したり、メソッドの戻り値として直接返したりすることが可能です。

switch式の構文的特徴

switch式では、キーワードの配置が変わり、ラムダ演算子に似た => を使用します。

また、casebreak キーワードが不要になり、コードの行数を大幅に削減できます。

C#
using System;

class Program
{
    static void Main()
    {
        int dayOfWeek = 3;

        // switch式による値の決定
        string message = dayOfWeek switch
        {
            1 => "月曜日:週の始まりです。",
            2 => "火曜日:作業に集中しましょう。",
            3 => "水曜日:週の折り返し地点です。",
            4 => "木曜日:あと少しで週末です。",
            5 => "金曜日:ラストスパートです。",
            _ => "週末:ゆっくり休みましょう。" // _ はデフォルトケース(ディスカード)
        };

        Console.WriteLine(message);
    }
}
実行結果
水曜日:週の折り返し地点です。

switch式のメリット

switch式を利用することで、以下のような利点が得られます。

  • 簡潔性: ボイラープレートコード(定型文)が排除され、ロジックそのものに集中できます。
  • 代入の直感性: 条件分岐の結果を変数に格納する際、外部で変数を作成して各case内で代入する手間が省けます。
  • 網羅性のチェック: コンパイラは、全ての可能性(特に列挙型の場合)がカバーされているかをチェックしやすくなり、漏れがある場合に警告を出してくれます。

パターンマッチングによる柔軟な条件分岐

C#のswitchが強力なのは、パターンマッチング と呼ばれる機能と高度に統合されているからです。

これにより、単一の値の比較を超えた複雑な判定が可能になります。

型パターン(Type Pattern)

オブジェクトが特定の型であるかどうかを判定し、同時にその型の変数としてキャストする手法です。

これは、共通の基底クラスやインターフェースを持つ異なるオブジェクトを処理する際に極めて有効です。

C#
public abstract class Shape { }
public class Circle : Shape { public double Radius { get; set; } }
public class Rectangle : Shape { public double Width { get; set; }; public double Height { get; set; } }

public static void PrintArea(Shape shape)
{
    switch (shape)
    {
        case Circle c:
            Console.WriteLine($"円の面積: {Math.PI * c.Radius * c.Radius}");
            break;
        case Rectangle r:
            Console.WriteLine($"長方形の面積: {r.Width * r.Height}");
            break;
        case null:
            throw new ArgumentNullException(nameof(shape));
        default:
            Console.WriteLine("未知の図形です。");
            break;
    }
}

プロパティパターン(Property Pattern)

オブジェクトの特定のプロパティが特定の値を持っているかどうかをチェックします。

オブジェクト全体をキャストするだけでなく、その内部構造まで踏み込んで条件を指定できます。

C#
public record User(string Name, string Role, bool IsActive);

public string GetAccessLevel(User user) => user switch
{
    { Role: "Admin", IsActive: true } => "フルアクセス権限",
    { Role: "Editor", IsActive: true } => "編集権限",
    { IsActive: false } => "アカウント無効",
    _ => "ゲスト権限"
};

このように、波括弧 { } 内にプロパティ名と期待値を記述することで、複雑な条件を一行で表現できます。

高度なパターンマッチング:リレーショナルと論理パターン

C# 9.0以降では、比較演算子や論理演算子をswitch内で直接使用できるようになりました。

これにより、数値範囲の判定などが驚くほど読みやすくなりました。

リレーショナルパターン(Relational Pattern)

数値の大小比較(<, >, <=, >=)をcaseラベルやswitch式のパターンとして使用できます。

C#
static string GetAgeCategory(int age) => age switch
{
    < 0 => "エラー:無効な年齢です",
    < 13 => "子供",
    < 20 => "ティーンエイジャー",
    < 65 => "成人",
    _ => "高齢者"
};

論理パターン(Logical Pattern)

and, or, not キーワードを使用して、複数のパターンを組み合わせることができます。

C#
static string GetSeason(DateTime date) => date.Month switch
{
    3 or 4 or 5 => "春",
    6 or 7 or 8 => "夏",
    9 or 10 or 11 => "秋",
    12 or 1 or 2 => "冬",
    _ => throw new ArgumentOutOfRangeException()
};

static bool IsLetterOrSeparator(char c) => c switch
{
    >= 'a' and <= 'z' or >= 'A' and <= 'Z' => true,
    '.' or ',' or '!' or '?' => true,
    _ => false
};

注意点として、従来の論理演算子である &&|| ではなく、英単語の andor を使用する点に留意してください。

これは、パターンマッチング専用の構文として定義されているためです。

when句を用いたガード条件の指定

パターンマッチングだけでは表現しきれない動的な条件(他の変数の状態に依存する場合など)を記述するために、when キーワードを使用できます。

これを ガード句 と呼びます。

C#
public static decimal CalculateDiscount(Order order) => order switch
{
    { TotalAmount: > 10000 } when order.IsPremiumMember => 2000, // 1万円以上かつプレミアム会員
    { TotalAmount: > 10000 } => 1000,                           // 1万円以上
    { TotalAmount: > 5000 } => 500,                             // 5千円以上
    _ => 0
};

when句を使用することで、パターンの「形」だけでなく、その時の「状態」に基づいた柔軟な分岐が可能になります。

複数の値を組み合わせるタプルパターン

複数の変数をセットで判定したい場合、タプルパターン が非常に便利です。

これは、複数の入力値を一時的なタプルにまとめ、それに対してマッチングを行う手法です。

C#
using System;

class Program
{
    static string GetChoice(string player1, string player2) => (player1, player2) switch
    {
        ("Rock", "Paper") => "Player 2 wins!",
        ("Rock", "Scissors") => "Player 1 wins!",
        ("Paper", "Rock") => "Player 1 wins!",
        ("Paper", "Scissors") => "Player 2 wins!",
        ("Scissors", "Rock") => "Player 2 wins!",
        ("Scissors", "Paper") => "Player 1 wins!",
        (var p1, var p2) when p1 == p2 => "Draw!",
        _ => "Invalid input"
    };

    static void Main()
    {
        Console.WriteLine(GetChoice("Rock", "Scissors"));
        Console.WriteLine(GetChoice("Paper", "Paper"));
    }
}
実行結果
Player 1 wins!
Draw!

この例では、二人のプレイヤーの手をタプル (player1, player2) としてまとめ、全パターンの組み合わせを一目瞭然の形式で記述しています。

リストパターン:配列やコレクションの構造マッチング

C# 11で導入された リストパターン を使用すると、配列やリストの要素数や特定の要素の配置に基づいて分岐させることができます。

C#
int[] numbers = { 1, 2, 3 };

string result = numbers switch
{
    [] => "空のリスト",
    [1, 2, 3] => "1, 2, 3の並び",
    [1, ..] => "1で始まるリスト",
    [.., 10] => "10で終わるリスト",
    [var first, _, var last] => $"最初が{first}、最後が{last}の3要素リスト",
    _ => "その他のリスト"
};

Console.WriteLine(result);

.. (スライスパターン) を使用することで、任意の数の要素をスキップしてマッチングを行うことも可能です。

これにより、CSVデータのパースや通信プロトコルの解析コードが劇的に簡潔になります。

switch文とswitch式の使い分けと注意点

非常に強力なswitch機能ですが、全てのケースでswitch式を使えば良いというわけではありません。

状況に応じた適切な使い分けが求められます。

使い分けの指針

特徴switch文 (Statement)switch式 (Expression)
主な用途複雑な処理の実行、状態変更値の算出、代入、返却
構文case, break を使用=>, _ を使用
実行内容複数の命令文を実行可能単一の式(単一の値)のみ
可読性処理が長い場合に適しているロジックが簡潔な場合に適している

基本的には、値を返すことが目的であればswitch式を、副作用のある処理(ログ出力やメソッド呼び出しの羅列)を行うのであればswitch文を選択するのがベストプラクティスです。

パフォーマンスに関する考慮事項

多くの場合、switch文とswitch式のパフォーマンス差は無視できるほど微小です。

C#コンパイラは、caseの数や値の密度に応じて、ジャンプテーブルやハッシュテーブルを用いた最適化を自動的に行います。

ただし、パターンマッチングを多用する場合、特に when 句で重いメソッド呼び出しを行ったり、複雑なプロパティアクセスを繰り返したりすると、その評価コストが無視できなくなる可能性があります。

クリティカルなループ内では、判定条件を可能な限り単純な定数比較に留める工夫が必要です。

網羅性の保証

switch式を使用する際、全ての入力パターンがカバーされていないとコンパイラが警告を出します。

例えば、列挙型(enum)を使用している場合、新しい要素を追加した際にswitch式の修正を忘れると警告が発生するため、実行時の予期せぬバグを未然に防ぐことができます。

これは、if-else文にはない大きなメリットの一つです。

まとめ

C#のswitch case文は、単なる条件分岐の手段から、データの「形」を宣言的に扱うための強力なパターンマッチングツールへと進化しました。

従来のswitch文は、手続き的な処理を記述する際に今でも有用ですが、現代的なC#開発においては、switch式パターンマッチング を活用することで、より宣言的で読みやすく、かつ堅牢なコードを記述できます。

特に、型パターンやリレーショナルパターン、リストパターンといった新機能を使いこなすことは、保守性の高いアプリケーションを開発する上で欠かせないスキルと言えるでしょう。

まずは、日常的に書いている if (obj is Type t)if (x > 0 && x < 10) といった条件式を、switch式やパターンマッチングで書き換えられないか検討することから始めてみてください。

コードの意図がより明確になり、C#の持つ表現力を最大限に引き出せるようになるはずです。