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

"パターン マッチング" は、式をテストして特定の特性があるかどうかを判断する手法です。 C# のパターン マッチングでは、式をテストし、式が一致した場合にアクションを実行するための、より簡潔な構文が提供されています。 "is 式" では、式をテストし、その式の結果に対して条件付きで新しい変数を宣言する、パターン マッチングがサポートされます。 "switch 式" を使用すると、式の最初の一致パターンに基づいてアクションを実行できます。 これら 2 つの式により、豊富な "パターン" のボキャブラリがサポートされています。

この記事では、パターン マッチングを使用できるシナリオの概要について説明します。 これらの手法によって、コードの読みやすさと正確性が向上します。 適用できるすべてのパターンの詳細については、言語リファレンスのパターンに関する記事を参照してください。

null チェック

パターン マッチングの最も一般的なシナリオの 1 つは、値が null ではないことを確認することです。 次の例を使用して null かどうかをテストしながら、null 許容値型をテストし、基になる型に変換することができます。

int? maybe = 12;

if (maybe is int number)
{
    Console.WriteLine($"The nullable int 'maybe' has the value {number}");
}
else
{
    Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
}

上記のコードは、変数の型をテストして新しい変数に代入する宣言パターンです。 言語規則によって、この手法は他の多くの方法よりも安全になります。 変数 number にアクセスして代入できるのは、if 句の true の部分だけです。 else 句や、if ブロックの後など、別の場所でそれにアクセスしようとすると、コンパイラによってエラーが発行されます。 さらに、== 演算子を使用していないため、型で == 演算子がオーバーロードされていても、このパターンは機能します。 これにより、not パターンを追加して、null 参照値を確認するのに理想的な方法になります。

string? message = ReadMessageOrDefault();

if (message is not null)
{
    Console.WriteLine(message);
}

前の例では、"定数パターン" を使用して、変数を null と比較しました。 not は "論理パターン" であり、否定されたパターンが一致しない場合に一致します。

型のテスト

パターン マッチングのもう 1 つの一般的な用途は、変数をテストして、特定の型と一致するかどうかを確認する場合です。 たとえば、次のコードを使用すると、変数が null ではないかどうかがテストされ、System.Collections.Generic.IList<T> インターフェイスが実装されます。 一致する場合は、そのリストで ICollection<T>.Count プロパティを使用して中央のインデックスを検索します。 宣言パターンの場合は、変数のコンパイル時の型に関係なく、null 値と一致しません。 次のコードを使用すると、IList が実装されていない型に対する保護に加えて、null に対して保護されます。

public static T MidPoint<T>(IEnumerable<T> sequence)
{
    if (sequence is IList<T> list)
    {
        return list[list.Count / 2];
    }
    else if (sequence is null)
    {
        throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
    }
    else
    {
        int halfLength = sequence.Count() / 2 - 1;
        if (halfLength < 0) halfLength = 0;
        return sequence.Skip(halfLength).First();
    }
}

同じテストを switch 式で適用すると、複数の異なる型に対して変数をテストすることができます。 その情報を使用して、特定の実行時の型に基づく、より適切なアルゴリズムを作成できます。

離散値を比較する

変数をテストして、特定の値との一致を見つけることもできます。 次のコードは、列挙型で宣言されているすべての可能な値に対して値をテストする 1 つの例を示したものです。

public State PerformOperation(Operation command) =>
   command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
   };

前の例では、列挙型の値に基づくメソッドのディスパッチが示されています。 最後の _ のケースは、すべての値と一致する "破棄パターン" です。 定義されているどの enum 値とも値が一致しないエラー状態が処理されます。 その switch アームを省略した場合、可能性のある入力値の中に、パターン式で処理されないものがあることが、コンパイラによって警告されます。 実行時に、検査対象のオブジェクトが switch アームのいずれとも一致しない場合、switch 式で例外がスローされます。 列挙値のセットの代わりに、数値定数を使用できます。 また、コマンドを表す定数文字列値に対して、これと同様の手法を使用することもできます。

public State PerformOperation(string command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

前の例では同じアルゴリズムが示されていますが、列挙型ではなく文字列値が使用されています。 通常のデータ形式ではなくテキスト コマンドに応答するアプリケーションの場合は、このシナリオを使用します。 C# 11 以降では、次の例のように、Span<char> または ReadOnlySpan<char> を使用し、定数文字列値をテストすることもできます。

public State PerformOperation(ReadOnlySpan<char> command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

これらのすべての例で、"破棄パターン" によって、すべての入力が確実に処理されます。 可能性のあるすべての入力値が処理されているかどうかが、コンパイラによって確認されます。

リレーショナル パターン

"リレーショナル パターン" を使用すると、値と定数の比較方法をテストできます。 たとえば、次のコードからは、カ氏の温度に基づいて水の状態が返されます。

string WaterState(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        (> 32) and (< 212) => "liquid",
        < 32 => "solid",
        > 212 => "gas",
        32 => "solid/liquid transition",
        212 => "liquid / gas transition",
    };

上のコードでは、両方のリレーショナル パターンが一致することを調べるための、接続的 and "論理パターン" も示されています。 また、離接的 or パターンを使用して、いずれかのパターンが一致することを調べることもできます。 2 つのリレーショナル パターンはかっこで囲まれており、わかりやすくするために任意のパターンでそれを使用できます。 最後の 2 つの switch アームによって、融点と沸点が処理されます。 これら 2 つのアームがないと、ロジックがすべての可能な入力に対応していないことが、コンパイラによって警告されます。

上記のコードは、パターン マッチング式に対してコンパイラが提供するもう 1 つの重要な機能も示しています。すべての入力値を処理しない場合、コンパイラは警告を出します。 コンパイラは、switch アームのパターンが、前のパターンによってカバーされている場合も警告を表示します。 これにより、switch 式のリファクタリングと並べ替えを自由に行えます。 同じ式を記述するもう 1 つの方法を次に示します。

string WaterState2(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        < 32 => "solid",
        32 => "solid/liquid transition",
        < 212 => "liquid",
        212 => "liquid / gas transition",
        _ => "gas",
};

前のサンプル、およびその他のリファクタリングや並べ替えでの重要な教訓は、コンパイラは、可能性のあるすべての入力がコードによって処理されるのを検証する、ということです。

複数の入力

これまでのパターンはすべて、1 つの入力をチェックするものでした。 オブジェクトの複数のプロパティを調べるパターンを作成できます。 次のような Order レコードについて考えます。

public record Order(int Items, decimal Cost);

上の位置指定レコード型では、明示的な位置で 2 つのメンバーが宣言されています。 最初にあるのは Items で、次に注文の Cost があります。 詳細は、レコードに関するページ参照してください。

次のコードでは、商品の数と注文の値を調べて、割引価格を計算しています。

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        { Items: > 10, Cost: > 1000.00m } => 0.10m,
        { Items: > 5, Cost: > 500.00m } => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

最初の 2 つのアームによって、Order の 2 つのプロパティが調べられます。 3 番目では、コストだけが調べられます。 次に null かどうかをチェックし、最後は他のすべての値と一致します。 Order 型で適切な Deconstruct メソッドが定義されている場合、パターンからプロパティ名を省略し、分解を使用してプロパティを調べることができます。

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        ( > 10,  > 1000.00m) => 0.10m,
        ( > 5, > 50.00m) => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

上のコードでは、プロパティが式に対して分解される "位置指定パターン" が示されています。

リスト パターン

"リスト パターン" を使うと、リストまたは配列の要素を確認できます。 リスト パターンを使うと、シーケンスの任意の要素にパターンを適用することができます。 さらに、"破棄パターン" (_) を適用して任意の要素と照合することや、"スライス パターン" を適用して 0 個以上の要素と照合することができます。

リスト パターンは、データが通常の構造に従っていない場合に役立つツールです。 パターン マッチングを使うと、データをオブジェクト セットに変換するのではなく、データの形状や値をテストすることができます。

銀行取引を含むテキスト ファイルからの次の抜粋を考えてみましょう。

04-01-2020, DEPOSIT,    Initial deposit,            2250.00
04-15-2020, DEPOSIT,    Refund,                      125.65
04-18-2020, DEPOSIT,    Paycheck,                    825.65
04-22-2020, WITHDRAWAL, Debit,           Groceries,  255.73
05-01-2020, WITHDRAWAL, #1102,           Rent, apt, 2100.00
05-02-2020, INTEREST,                                  0.65
05-07-2020, WITHDRAWAL, Debit,           Movies,      12.57
04-15-2020, FEE,                                       5.55

これは CSV 形式ですが、一部の行は他の行よりも列数が多くなっています。 処理にとってさらに都合が悪いのは、WITHDRAWAL 型のある列に、ユーザーが生成したテキストが含まれ、そのテキストに、コンマが含まれる可能性が含まれることです。 値をキャプチャするために "破棄" パターン、"定数" パターン、"変数" パターンなどの "リスト パターン" を使うと、次の形式のデータを処理できます。

decimal balance = 0m;
foreach (string[] transaction in ReadRecords())
{
    balance += transaction switch
    {
        [_, "DEPOSIT", _, var amount]     => decimal.Parse(amount),
        [_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
        [_, "INTEREST", var amount]       => decimal.Parse(amount),
        [_, "FEE", var fee]               => -decimal.Parse(fee),
        _                                 => throw new InvalidOperationException($"Record {string.Join(", ", transaction)} is not in the expected format!"),
    };
    Console.WriteLine($"Record: {string.Join(", ", transaction)}, New balance: {balance:C}");
}

前の例では、文字列配列を受け取ります。各要素は行の 1 つのフィールドです。 switch 式は、取引の種類を決定する 2 つ目のフィールドと、その他の列数をキーにしています。 各行で、データが正しい形式であることが確認されます。 破棄パターン (_) の場合、取引の日付を含む最初のフィールドをスキップします。 2 つ目のフィールドは、取引の種類と一致します。 その他の要素の一致は、金額を含むフィールドまでスキップされます。 最後の一致では、"変数" パターンを使って、金額の文字列表現をキャプチャします。 この式を使うと、残高から加算または減算する金額を計算できます。

"リスト パターン" を使うと、データ要素のシーケンスの形状に対して照合することができます。 要素の位置を照合するには、"破棄" と "スライス" のパターンを使います。 個々の要素に関する特性と照合するには、その他のパターンを使います。

この記事では、C# のパターン マッチングを使用して記述できるコードの種類について説明しました。 次の記事では、シナリオでパターンを使用する他の例と、使用可能なパターンの完全なボキャブラリが示されています。

関連項目