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

"パターン マッチング" は、式をテストして特定の特性があるかどうかを判断する手法です。 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 = "This is not the null string";

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)),
   };

前の例では同じアルゴリズムが示されていますが、列挙型ではなく文字列値が使用されています。 通常のデータ形式ではなくテキスト コマンドに応答するアプリケーションの場合は、このシナリオを使用します。 これらのすべての例で、"破棄パターン" によって、すべての入力が確実に処理されます。 可能性のあるすべての入力値が処理されているかどうかが、コンパイラによって確認されます。

リレーショナル パターン

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

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 アームが既に処理されている場合も、コンパイラは警告を発行します。 これにより、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,
        Order { 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,
        Order { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

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

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

関連項目