C#は、バージョンを重ねるごとに進化を続けており、その中でも開発者の生産性とコードの可読性を劇的に向上させた機能が「パターンマッチング」です。

かつては冗長になりがちだった型判定や条件分岐が、パターンマッチングの導入によって簡潔かつ宣言的に記述できるようになりました。

本記事では、C# 7.0から導入され、最新バージョンであるC# 12やC# 13に至るまで強化され続けてきたパターンマッチングの全容を解説します。

基本的な is 演算子の使い方から、実戦で役立つ switch 式、そして高度なリストパターンまで、具体的なコード例を交えて詳しく見ていきましょう。

C#パターンマッチングの概要

パターンマッチングとは、値が特定の「パターン」に適合するかどうかを判定し、適合した場合にはその値から情報を抽出する機能のことです。

従来のC#では、オブジェクトの型をチェックしてキャストを行う際、if (obj is MyClass) と書いた後に var c = (MyClass)obj; といった二段構えの処理が必要でした。

パターンマッチングはこの手続きを一段階に凝縮します。

単なる型チェックに留まらず、プロパティの値、数値の範囲、さらには配列の構造までを直感的に記述できるのが特徴です。

これにより、コードの意図が明確になり、バグの混入を防ぐ効果も期待できます。

is演算子による型パターンと宣言パターン

最も基本的かつ頻繁に利用されるのが、is 演算子を用いたパターンマッチングです。

これは「型パターン」と呼ばれ、オブジェクトが特定の型であるかを確認すると同時に、その型の変数として導入することができます。

型チェックと変数宣言の統合

従来の as 演算子や型キャストと比較して、パターンマッチングを用いた記述は非常にスマートです。

C#
using System;

public class PatternMatchingDemo
{
    public static void Main()
    {
        object myValue = "Hello, C# Pattern Matching!";

        // 型パターンを使用して型をチェックし、同時に変数 'str' に代入する
        if (myValue is string strValue)
        {
            // このブロック内では strValue が string 型として使用可能
            Console.WriteLine($"文字列の長さは: {strValue.Length}");
        }
        else
        {
            Console.WriteLine("指定された値は文字列ではありません。");
        }
    }
}
実行結果
文字列の長さは: 28

この例では、myValue is string strValue という記述により、「myValueがstring型であれば、それをstrValueという名前の変数に格納する」という操作を一行で行っています。

もし myValuenull の場合、このパターンは不適合(false)となるため、nullチェックも同時に行われていることになります。

varパターンと破棄パターン

var パターンは、どんな値にも一致し、その値を新しい変数に代入します。

一方、_ (アンダースコア)を用いた「破棄パターン」は、値には一致させたいが、その値自体は後続の処理で使わない場合に使用します。

C#
// varパターンの例(常に真となるが、中間結果を保持したい場合に便利)
if (CalculateValue() is var result && result > 100)
{
    Console.WriteLine($"結果は100を超えています: {result}");
}

// 破棄パターンの例(型さえ合っていれば中身はどうでもいい場合)
if (input is string _)
{
    Console.WriteLine("入力は文字列です(内容は無視します)");
}

switch式による多分岐の最適化

C# 8.0で導入された switch 式は、パターンマッチングの真価を発揮する機能の一つです。

従来の switch 文と異なり、値を返す「式」として記述できるため、代入操作やメソッドの戻り値として直接利用可能です。

switch文とswitch式の違い

まずは、従来の switch 文と新しい switch 式を比較してみましょう。

特徴switch文 (Statement)switch式 (Expression)
記述形式switch (x) { case ... }x switch { ... }
結果の返却できない(代入が必要)できる(直接値を返す)
キーワードcase, break, default が必要不要(ラムダ形式 => を使用)
網羅性チェック弱い(コンパイラ警告が限定的)強い(すべてのパターンを網羅しないと警告)

switch式の実装例

以下は、図形の面積を計算する処理を switch 式で記述した例です。

C#
using System;

public record Circle(double Radius);
public record Rectangle(double Width, double Height);
public record Triangle(double Base, double Height);

public class GeometryCalculator
{
    public static double GetArea(object shape) => shape switch
    {
        Circle c => Math.PI * Math.Pow(c.Radius, 2),
        Rectangle r => r.Width * r.Height,
        Triangle t => 0.5 * t.Base * t.Height,
        null => throw new ArgumentNullException(nameof(shape)),
        _ => throw new NotSupportedException("未知の図形です") // デフォルトケース
    };

    public static void Main()
    {
        var myCircle = new Circle(10);
        Console.WriteLine($"円の面積: {GetArea(myCircle):F2}");

        var myRect = new Rectangle(5, 10);
        Console.WriteLine($"長方形の面積: {GetArea(myRect)}");
    }
}
実行結果
円の面積: 314.16
長方形の面積: 50

switch式では、従来の「case」キーワードを使わずに、パターンと結果を「=>」で結びます。

また、最後に _ を指定することで、どのパターンにも当てはまらなかった場合のデフォルト動作を定義します。

プロパティパターンと位置指定パターン

パターンマッチングは、オブジェクトの内部構造にまで踏み込むことができます。

これにより、特定のプロパティが特定の条件を満たしているかどうかを簡潔に判定できます。

プロパティパターン

オブジェクトのプロパティを直接参照してマッチングを行う手法です。

ネストされたプロパティの参照も可能です。

C#
public record Address(string State, string City);
public record Customer(string Name, Address Address);

public class ShippingService
{
    public static decimal GetShippingCost(Customer customer) => customer switch
    {
        // プロパティ State が "Tokyo" である場合にマッチ
        { Address: { State: "Tokyo" } } => 0m,
        // ネストされたプロパティへのアクセス(C# 10以降)
        { Address.State: "Osaka" } => 500m,
        // 全ての顧客にマッチ
        { } => 1000m,
        null => throw new ArgumentNullException(nameof(customer))
    };
}

このように、{ PropertyName: pattern } という形式で記述します。

C# 10からは Address.State のようにドットで繋いで記述できるようになり、さらに可読性が向上しました。

位置指定パターン(位置パターン)

型に Deconstruct メソッドが定義されている場合(record型など)、タプルのように値を展開してマッチングを行うことができます。

これを「位置指定パターン」と呼びます。

C#
using System;

public record Point(int X, int Y);

public class PositionTracker
{
    public static string CheckPosition(Point p) => p switch
    {
        (0, 0) => "原点にいます",
        (_, 0) => "X軸上にいます",
        (0, _) => "Y軸上にいます",
        var (x, y) when x > 0 && y > 0 => "第一象限にいます",
        _ => "その他の場所にいます"
    };

    public static void Main()
    {
        var pt = new Point(5, 5);
        Console.WriteLine(CheckPosition(pt));
    }
}
実行結果
第一象限にいます

when 句(ガード句)を組み合わせることで、パターンに適合した上でさらに詳細な条件式(bool条件)を追加することが可能です。

論理パターンとリレーショナルパターン

C# 9.0からは、数値を比較する「リレーショナルパターン」や、複数のパターンを組み合わせる「論理パターン」が導入されました。

これにより、if文の複雑な条件分岐を switch 式に完全に置き換えることが可能になりました。

リレーショナルパターン

比較演算子(<, <=, >, >=)を使用して値を評価します。

論理パターン (and, or, not)

複数のパターンを論理演算子で結合します。

特に not パターンは、nullチェックにおいて if (obj is not null) と書けるようになるため、非常に重宝されます。

C#
public static string GetGrade(int score) => score switch
{
    >= 90 => "秀",
    >= 80 and < 90 => "優",
    >= 70 and < 80 => "良",
    >= 60 and < 70 => "可",
    < 60 and >= 0 => "不可",
    _ => "無効なスコア"
};

この記法の素晴らしい点は、「変数を繰り返し記述する必要がない」点です。

従来のif文であれば score >= 80 && score < 90 と書く必要がありましたが、パターンマッチングでは対象となる変数が自明であるため、より自然言語に近い形で条件を記述できます。

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

C# 11で導入された「リストパターン」は、パターンマッチングの可能性をさらに広げました。

これは、配列やリストといったコレクションの要素数や特定のインデックスにある値をパターンとして記述できる機能です。

基本的なリストパターンの使い方

リストパターンはブラケット [] を使用して記述します。

C#
using System;

public class ListPatternDemo
{
    public static string AnalyzeArray(int[] numbers) => numbers switch
    {
        [] => "空の配列です",
        [1, 2, 3] => "1, 2, 3 の配列です",
        [var first, _, var last] => $"3要素の配列です。最初: {first}, 最後: {last}",
        [1, .. var rest] => $"1で始まり、その後に {rest.Length} 個の要素が続きます",
        [..] => "任意の配列です"
    };

    public static void Main()
    {
        Console.WriteLine(AnalyzeArray(new int[] { }));
        Console.WriteLine(AnalyzeArray(new int[] { 1, 2, 3 }));
        Console.WriteLine(AnalyzeArray(new int[] { 10, 20, 30 }));
        Console.WriteLine(AnalyzeArray(new int[] { 1, 5, 6, 7 }));
    }
}
実行結果
空の配列です
1, 2, 3 の配列です
3要素の配列です。最初: 10, 最後: 30
1で始まり、その後に 3 個の要素が続きます

スライスパターン (..)

リストパターンの中で特に強力なのが、.. で表される「スライスパターン」です。

これは 「0個以上の任意の要素」にマッチします。

  • [1, .. , 10] : 1で始まり10で終わる、任意の長さの配列にマッチ。
  • [.. var middle] : 配列の全ての要素を middle にキャプチャ。

CSVデータの解析や、固定長フォーマットの電文処理など、特定の構造を持つデータの検証においてリストパターンは圧倒的な威力を発揮します。

パターンマッチングのベストプラクティス

パターンマッチングは非常に強力ですが、使いどころを誤ると逆にコードを複雑にしてしまう可能性があります。

効果的に活用するためのポイントをいくつか挙げます。

1. nullチェックの統一

C#における現代的なnullチェックの標準は is not null です。

演算子のオーバーロードの影響を受けず、意図が明確になります。

C#
if (item is not null)
{
    // 安全にアクセス可能
}

2. switch式の網羅性を活用する

switch 式は、列挙型(enum)と組み合わせると非常に強力です。

新しい列挙子を追加した際、その値を処理していない switch 式があればコンパイラが警告を出してくれるため、修正漏れを防ぐことができます。

3. 過度なネストを避ける

プロパティパターンを深くネストさせすぎると、可読性が低下します。

ネストが3段階を超えるような場合は、ロジックを分割するか、ガード句(when)の利用を検討してください。

4. 型によるディスパッチを優先する

オブジェクト指向のポリモーフィズム(多態性)で解決できる問題を、すべて switch 式で解決しようとするのは避けましょう。

ただし、外部ライブラリのクラスなど、自分が所有していないクラス階層に対して振る舞いを追加する場合は、パターンマッチングが最適な解決策となります。

まとめ

C#のパターンマッチングは、単なるシンタックスシュガーを超え、現代的なC#プログラミングにおける中核的な機能となりました。

is 演算子による安全なキャストから始まり、switch 式による宣言的なロジック記述、そして高度なリストパターンによる構造解析まで、その進化は止まりません。

これらの機能を使いこなすことで、「短く、読みやすく、堅牢な」コードを書くことが可能になります。

特に、これまで複雑な if-else 構文や入れ子になった switch 文で苦労していた箇所があれば、ぜひ最新のパターンマッチングを使ってリファクタリングに挑戦してみてください。

C#は今後もさらにパターンマッチングの機能を洗練させていくことが予想されます。

常に最新の言語仕様に注目し、より洗練されたコーディングスタイルを追求していきましょう。