C#というプログラミング言語は、ここ数年のアップデートによって劇的な進化を遂げました。
その中でも特に開発者のコーディングスタイルを大きく変えたのが、パターンマッチングの導入と強化です。
かつての C# における switch 文は、特定の定数値に基づいて処理を分岐させるための単純な制御構文に過ぎませんでした。
しかし、現在の C# では、型、プロパティ、論理条件、さらにはコレクションの構造までもが、簡潔かつ強力な構文で記述できるようになっています。
この記事では、モダンな C# 開発において必須知識となった switch 文および switch 式を用いたパターンマッチングについて、基礎から最新の応用テクニックまでを詳しく解説します。
冗長なコードを排除し、読みやすく保守性の高いコードを書くための実践的なノウハウを身に付けていきましょう。
switch文の基本と進化の歴史
C# の初期から存在する switch 文は、複数の分岐を処理するための基本的な仕組みでした。
まずは、従来からある switch 文の役割を整理し、最新のパターンマッチングがどのような課題を解決するために生まれたのかを振り返ります。
従来のswitch文(C# 6以前)
C# 6以前の switch 文は、整数型や文字列型などの 定数値との比較に限定されていました。
そのため、オブジェクトの「型」で分岐させたり、複数の条件を組み合わせたりするには、if-else 文を何層にも重ねる必要がありました。
// 従来の古い書き方
object data = 123;
switch (data)
{
case int i: // これはC# 7以降で可能になった型パターン
Console.WriteLine($"数値です: {i}");
break;
case string s:
Console.WriteLine($"文字列です: {s}");
break;
default:
Console.WriteLine("未知の型です");
break;
}
パターンマッチングの導入(C# 7.0〜)
C# 7.0 以降、switch 文の中で型を確認し、そのまま変数に代入する 型パターン が導入されました。
これにより、キャスト操作の手間が省け、コードの安全性が飛躍的に向上しました。
さらに、when 句を用いた「ガード条件」が追加されたことで、特定の値や状態に基づく詳細な分岐が可能になりました。
これが、現在の高度なパターンマッチングの原点となっています。
現代的な「switch式」による宣言的な記述
C# 8.0 で導入された switch 式 (switch expressions) は、コードの書き方を根底から変えました。
従来の switch 文が「文 (Statement)」として処理の流れを制御するものであったのに対し、switch 式は「式 (Expression)」として値を返します。
switch式の基本構文
switch 式を用いると、case や break といったキーワードを省略し、ラムダ式のようなアロー演算子 => を使って記述できます。
using System;
public enum OrderStatus { Pending, Shipped, Delivered, Cancelled }
public class Program
{
public static void Main()
{
OrderStatus status = OrderStatus.Shipped;
// switch式による値の割り当て
string message = status switch
{
OrderStatus.Pending => "注文は保留中です",
OrderStatus.Shipped => "商品は発送済みです",
OrderStatus.Delivered => "商品は配達完了しました",
OrderStatus.Cancelled => "注文はキャンセルされました",
_ => "不明なステータスです" // デフォルトケース(破棄)
};
Console.WriteLine(message);
}
}
商品は発送済みです
switch式のメリット
switch 式を採用することで、以下のメリットが得られます。
- コードの簡潔化:ボイラープレート(定型文)が減り、ロジックの本質が明確になります。
- 網羅性のチェック:すべてのケースがカバーされていない場合、コンパイラが警告を出してくれるため、バグの混入を防げます。
- 読み取り専用(イミュータブル)な設計:変数に値を直接代入できるため、
readonlyフィールドの初期化などにも適しています。
多彩なパターンマッチングの種類
現代の C# では、単なる値の比較に留まらない、多種多様なパターンを利用できます。
これらを組み合わせることで、複雑な条件分岐を驚くほどシンプルに記述可能です。
1. プロパティ・パターン
オブジェクトの特定のプロパティの状態に基づいて分岐させる手法です。
オブジェクト全体を調べるのではなく、内部の値のみに焦点を当てることができます。
public record Customer(string Name, bool IsPremium, int PurchaseHistoryCount);
public static decimal GetDiscount(Customer customer) => customer switch
{
// プロパティの値を確認
{ IsPremium: true, PurchaseHistoryCount: > 10 } => 0.2m,
{ IsPremium: true } => 0.1m,
{ PurchaseHistoryCount: > 5 } => 0.05m,
_ => 0m
};
2. リレーショナル・パターン(関係パターン)
C# 9.0 以降、比較演算子(<, <=, >, >=)を直接パターンとして使用できるようになりました。
数値の範囲を扱う際に非常に便利です。
public static string GetAgeCategory(int age) => age switch
{
< 13 => "子供",
< 20 => "ティーンエイジャー",
< 65 => "成人",
_ => "シニア"
};
3. ロジカル・パターン(論理パターン)
複数の条件を and, or, not で結合できます。
従来の && や || よりも自然言語に近い形で記述できるのが特徴です。
public static bool IsWorkingDay(DateTime date) => date.DayOfWeek switch
{
DayOfWeek.Saturday or DayOfWeek.Sunday => false,
not DayOfWeek.Monday => true, // 月曜以外
_ => true
};
特に not パターンは、null チェックにおいて非常に強力な威力を発揮します。
if (obj is not null) という記述は、現代の C# における標準的な null チェックの作法となりました。
高度なパターンマッチング:位置パターンとリストパターン
より複雑なデータ構造を扱うためのパターンも用意されています。
これらは、データの「形」そのものを判定材料にします。
位置パターン (Positional Patterns)
型の分解(Deconstruct)を利用して、オブジェクトの中身をタプルのように取り出しながらマッチングを行います。
public record Point(int X, int Y);
public static string GetQuadrant(Point p) => p switch
{
(0, 0) => "原点",
( > 0, > 0) => "第1象限",
( < 0, > 0) => "第2象限",
( < 0, < 0) => "第3象限",
( > 0, < 0) => "第4象限",
_ => "軸上"
};
リスト・パターン (List Patterns)
C# 11 で導入された リスト・パターン は、配列やリストの要素数や特定の要素の並びをチェックするための機能です。
int[] numbers = { 1, 2, 3 };
string result = numbers switch
{
[] => "空のリスト",
[1, 2, 3] => "1, 2, 3の並び",
[var first, .. var rest] => $"先頭は{first}、残りは{rest.Length}個",
_ => "その他"
};
.. (スライス・パターン)を使用することで、任意個数の要素をスキップしたり、残りの部分を取得したりすることが可能です。
これにより、CSV データの解析やパケットデータの処理などが極めて簡潔になります。
実践的な活用例:Web APIのレスポンス処理
実際のアプリケーション開発において、パターンマッチングがどのように役立つかを具体的なコード例で見てみましょう。
ここでは、API から返ってくる多種多様な結果を処理するシミュレーションを行います。
using System;
// レスポンスの状態を表すクラス群
public abstract class ApiResponse;
public record Success(string Data) : ApiResponse;
public record Error(int StatusCode, string Message) : ApiResponse;
public record Loading : ApiResponse;
public class ResponseHandler
{
public static void HandleResponse(ApiResponse response)
{
// 型とプロパティを組み合わせた高度なマッチング
string displayMessage = response switch
{
Success { Data: var content } => $"取得成功: {content}",
Error { StatusCode: 404 } => "エラー: リソースが見つかりませんでした",
Error { StatusCode: >= 500 } e => $"サーバーエラーが発生しました: {e.Message}",
Error e => $"クライアントエラー({e.StatusCode}): {e.Message}",
Loading => "読み込み中です...",
null => "レスポンスがありません",
_ => "未知のレスポンスタイプです"
};
Console.WriteLine(displayMessage);
}
public static void Main()
{
HandleResponse(new Success("ユーザー情報"));
HandleResponse(new Error(404, "Not Found"));
HandleResponse(new Error(502, "Bad Gateway"));
}
}
取得成功: ユーザー情報
エラー: リソースが見つかりませんでした
サーバーエラーが発生しました: Bad Gateway
このように、ApiResponse という抽象的な型を、具体的な型とその中身(ステータスコードなど)で一気にフィルタリングできるのがパターンマッチングの真骨頂です。
パターンマッチング利用時の注意点とベストプラクティス
強力なパターンマッチングですが、何でもかんでも switch 式に詰め込めば良いというわけではありません。
コードの可読性を維持するための注意点をいくつか挙げます。
1. 複雑すぎるパターンを避ける
一つの switch 句の中に、あまりにも多くの論理演算子やプロパティ・パターンを詰め込むと、かえって可読性が低下します。
条件が複雑になりすぎる場合は、メソッドとして抽出するか、事前に変数を整理することを検討してください。
2. 網羅性(Exhaustiveness)の確保
switch 式を使用する場合、すべてのパターンを網羅していないとランタイムエラー(SwitchExpressionException)が発生する可能性があります。
破棄パターン _ を適切に使用して、必ずデフォルトの挙動を定義しておくことが安全策です。
3. パターンの順序に注意する
パターンマッチングは 「上から順に評価」 されます。
より具体的な条件を上に、一般的な条件を下に記述しなければなりません。
// 誤った順序の例
int score = 95;
string grade = score switch
{
> 0 => "合格", // ここでマッチしてしまう
> 90 => "優秀", // この行は実行されない
_ => "不可"
};
このような場合、コンパイラが「到達不能なコード」として警告を出してくれることがほとんどですが、論理的なミスを防ぐためにも順序の意識は不可欠です。
まとめ
C# の switch 文とパターンマッチングは、単なる条件分岐の枠を超え、「データの構造を宣言的に記述する」ための強力なツールへと進化しました。
- switch式 を使うことで、コードを簡潔にし、副作用の少ない関数型の記述が可能になります。
- プロパティ・パターン や リレーショナル・パターン を活用すれば、複雑なビジネスロジックも直感的に表現できます。
- リスト・パターン や 位置パターン により、コレクションや複合データの解析が劇的に楽になります。
これらの機能を使いこなすことで、C# のコードはより安全で、読みやすく、そしてメンテナンスしやすいものへと変わります。
もし、まだ従来の if-else や古い switch 文を多用しているのであれば、ぜひ最新のパターンマッチングを取り入れたリファクタリングに挑戦してみてください。
その表現力の高さに、きっと驚くはずです。
