パターン マッチ

パターンでは、値に特定の "図形" を含まれているかどうかをテストし、一致する図形が含まれている場合にその値から情報を "抽出" することができます。 パターン マッチングでは、現在既に使用しているアルゴリズムに対してより簡潔な構文を提供します。 パターン マッチング アルゴリズムは、既存の構文を使用して今までも作成しています。 値をテストする if ステートメントまたは switch ステートメントを記述します。 その後、これらのステートメントで一致する値が見つかると、その値から情報を抽出して使用します。 新しい構文要素は、既に使い慣れているステートメントの拡張機能 isswitch です。 これらの新しい拡張機能は、値のテストとその情報の抽出を組み合わせたものです。

このトピックでは、新しい構文を紹介し、それをどのように使用すると読みやすく簡潔なコードを作成できるかを説明します。 パターン マッチングでは、データとデータを操作するメソッドが密接に結び付けられているオブジェクト指向設計とは異なり、データとコードが分離される表現形式が可能になります。

これらの新しい表現形式を説明するために、パターン マッチングのステートメントを使用して幾何学的図形を表す構造体を見ていきましょう。 おそらく、クラス階層の作成や、オブジェクトのランタイム型に基づいてオブジェクトの動作をカスタマイズするための仮想メソッドとオーバーライドされたメソッドの作成には慣れているでしょう。

これらの手法は、クラス階層で構造化されていないデータに対しては使うことができません。 データとメソッドが分離されている場合は、他のツールが必要になります。 新しい "パターン マッチング" コンストラクトを使用すると、より明確な構文でデータを検査し、そのデータの任意の条件に基づいて制御フローを操作できます。 変数の値をテストする if ステートメントと switch ステートメントを既に記述しました。 また、変数の型をテストする is ステートメントも記述しました。 "パターン マッチング" により、これらのステートメントに新しい機能が追加されます。

このトピックでは、さまざまな幾何学的図形の面積を計算するメソッドを作成します。 ただし、その際に、オブジェクト指向の手法を使用したり、各種図形に対応するクラス階層を構築したりしません。 代わりに、"パターン マッチング" を使用します。 継承を使用していないことをさらに強調するために、各図形をクラスではなく struct にします。 struct 型が異なると、共通のユーザー定義基本データ型を指定できないため、継承は可能な設計ではありません。 このサンプルを進めていく際に、このコードと、このコードをオブジェクト階層として構造化した場合を比較してください。 照会して操作する必要のあるデータがクラス階層ではない場合は、パターン マッチングを使うことで非常に洗練された設計が可能になります。

抽象的な図形の定義から開始して各種具体的な図形クラスを追加する代わりに、幾何学的図形それぞれの簡単なデータのみの定義から始めます。

public class Square
{
    public double Side { get; }

    public Square(double side)
    {
        Side = side;
    }
}
public class Circle
{
    public double Radius { get; }

    public Circle(double radius)
    {
        Radius = radius;
    }
}
public struct Rectangle
{
    public double Length { get; }
    public double Height { get; }

    public Rectangle(double length, double height)
    {
        Length = length;
        Height = height;
    }
}

public struct Triangle
{
    public double Base { get; }
    public double Height { get; }

    public Triangle(double @base, double height)
    {
        Base = @base;
        Height = height;
    }
}

これらの構造から、特定の図形の面積を計算するメソッドを記述してみましょう。

is 型パターンの式

C# 7 より前では、一連の if ステートメントと is ステートメント内のそれぞれの型をテストする必要がありました。

public static double ComputeArea(object shape)
{
    if (shape is Square)
    {
        var s = shape as Square;
        return s.Side * s.Side;
    } else if (shape is Circle)
    {
        var c = shape as Circle;
        return c.Radius * c.Radius * Math.PI;
    }
    // elided
    throw new ArgumentException(
        message: "shape is not a recognized shape",
        paramName: nameof(shape));
}

上記のコードは、従来の "型パターン" の式です。この場合、変数をテストしてその型を判別し、その型に基づいて別のアクションを実行しています。

このコードは、テストが成功した場合に is 式の拡張機能を使用して変数を代入することで、より簡潔になります。

public static double ComputeAreaModernIs(object shape)
{
    if (shape is Square s)
        return s.Side * s.Side;
    else if (shape is Circle c)
        return c.Radius * c.Radius * Math.PI;
    else if (shape is Rectangle r)
        return r.Height * r.Length;
    // elided
    throw new ArgumentException(
        message: "shape is not a recognized shape",
        paramName: nameof(shape));
}

この更新したバージョンでは、変数のテストと適切な型の新しい変数への代入の両方を is 式が実行します。 また、このバージョンには struct である Rectangle 型が含まれていることに注意してください。 新しい is 式は、値型と参照型で動作します。

パターン マッチング式の言語規則は、一致式の結果の誤った使用を回避することにも役立ちます。 上記の例では、変数 scr はスコープ内のみに存在し、それぞれのパターン マッチング式の結果が true のときに確実に代入されます。 別の場所でいずれかの変数を使用しようとすると、コンパイラ エラーが生成されます。

この 2 つの規則を詳しく調べてみましょう。まずはスコープです。 c 変数は、最初の if ステートメントの else 分岐のスコープ内のみにあります。 s 変数は、メソッド ComputeArea のスコープ内にあります。 これは、if ステートメントの各分岐によって変数に個別のスコープが確立されるためです。 ただし、if ステートメント自体はスコープを確立しません。 つまり、if ステートメントで宣言された変数は、if ステートメント (この場合はメソッド) と同じスコープにあります。この動作はパターン マッチングに固有のものではありませんが、変数スコープ、if ステートメント、else ステートメントに定義されている動作です。

c 変数と s 変数には、"true のときに確実に代入する" メカニズムにより、それぞれの if ステートメントが true のときに代入されます。

ヒント

このトピックのサンプルでは、推奨されるコンストラクトを使用しています。この場合、パターン マッチングの is 式により、if ステートメントの true 分岐で一致変数に確実に代入されます。 このロジックは、if (!(shape is Square s)) を記述することで反転できます。s 変数は、false 分岐でのみ、確実に代入されます。 これは有効な C# ですが、ロジックの追跡がわかりにくくなるため、お勧めしません。

これらの規則は、そのパターンを満たさなかったときにパターン マッチング式の結果に誤ってアクセスする可能性が低くなることを意味します。

パターン マッチング switch ステートメントの使用

時間が経過するにつれて、他の図形の種類をサポートすることが必要になる場合があります。 テストする条件の数が増えるにつれ、is パターン マッチング式の使用が煩雑になることもわかります。 確認する各型に対して if ステートメントが必要になるほか、is 式は、入力が単一の型と一致するかどうかをテストすることに限定されます。 この場合は、switch パターン マッチング式が適していることがわかります。

従来の switch ステートメントはパターン式であり、定数パターンをサポートしていました。 変数は、case ステートメントで使用されている任意の定数と比較することができました。

public static string GenerateMessage(params string[] parts)
{
    switch (parts.Length)
    {
        case 0:
            return "No elements to the input";
        case 1:
            return $"One element: {parts[0]}";
        case 2:
            return $"Two elements: {parts[0]}, {parts[1]}";
        default:
            return $"Many elements. Too many to write";
    }
}

switch ステートメントでサポートされるパターンは定数パターンのみでした。 さらに、このパターンは、数値型と string 型に限定されていました。 このような制限事項がなくなったため、型パターンを使用して switch ステートメントを記述できるようになりました。

public static double ComputeAreaModernSwitch(object shape)
{
    switch (shape)
    {
        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Rectangle r:
            return r.Height * r.Length;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

パターン マッチングの switch ステートメントでは、従来の C 形式の switch ステートメントを使用してきた開発者にとって使い慣れた構文を使用します。 それぞれの case が評価され、入力変数に一致する条件の下にあるコードが実行されます。 コードの実行では、ある case 式から次の case 式に "フォール スルーする" ことはできません。つまり、case ステートメントの構文では、それぞれの casebreakreturn、または goto で終わる必要があります。

注意

別のラベルに移動する goto ステートメントは、定数パターン (従来の switch ステートメント) のみに有効です。

switch ステートメントを制御する重要な新しい規則があります。 switch 式の変数の型に関する制限はなくなりました。 この例の object のように、どの型でも使用できます。 case 式は定数値に限定されなくなりました。 この制限がなくなるということは、switch セクションの順序を変更すると、プログラムの動作が変わる可能性があることを意味します。

定数値に限定されていたときは、switch 式の値と一致する case ラベルは 1 つだけでした。 各 switch セクションは次のセクションにフォール スルーできないという規則との組み合わせにより、switch セクションは、動作に影響しない任意の順序で並べ替えることができました。 現在は、より汎用的になった switch 式により、各セクションの順序が重要になります。 switch 式は、テキストの順序で評価されます。 実行は、switch 式に一致する最初の switch ラベルに移ります。
default ケースが実行されるのは、他の case ラベルが一致しない場合のみです。 default ケースは、テキストの順序に関係なく最後に評価されます。 default ケースがなく、他の case ステートメントのいずれも一致しない場合、実行は switch ステートメントの次のステートメントで続行されます。 case ラベルのコードは実行されません。

case 式の when

case ラベルで when 句を使用すると、面積が 0 の図形用に特殊なケースを作成できます。 辺の長さが 0 の正方形または半径が 0 の円は、面積が 0 になります。 その条件は、case ラベルで when 句を使用して指定します。

public static double ComputeArea_Version3(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

この変更には、新しい構文に関するいくつかの重要なポイントが示されています。 最初に、複数の case ラベルを 1 つの switch セクションに適用できます。 これらのラベルのいずれかが true のとき、ステートメント ブロックが実行されます。 この例では、switch 式が面積が 0 の円または正方形である場合、このメソッドが定数 0 を返します。

この例では、最初の switch ブロックの 2 つの case ラベルに異なる 2 つの変数を使用しています。 この switch ブロック内のステートメントで変数 c (円) または s (正方形) が使用されていないことに注意してください。 この switch ブロックでは、これらの変数のいずれも確実に代入されません。 これらのケースのいずれかが一致する場合は、変数の 1 つが明確に代入されています。 ただし、コンパイル時に "どれに" 代入されたかを通知することは不可能です。それは、実行時にいずれかのケースも一致する可能性があるためです。 そのため、同じブロックに複数の case ラベルを使用する場合のほとんどは、case ステートメントに新しい変数を導入しません。つまり、when 句の変数のみを使用します。

面積が 0 のこれらの図形を追加した後は、さらに図形の種類 (四角形と三角形) を追加してみましょう。

public static double ComputeArea_Version4(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
        case Triangle t when t.Base == 0 || t.Height == 0:
        case Rectangle r when r.Length == 0 || r.Height == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Triangle t:
            return t.Base * t.Height * 2;
        case Rectangle r:
            return r.Length * r.Height;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

この一連の変更により、低次元の場合用に case ラベル、新しい図形ごとにラベルとブロックが追加されます。

最後に、null ケースを追加して、引数が null にならないようにすることができます。

public static double ComputeArea_Version5(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
        case Triangle t when t.Base == 0 || t.Height == 0:
        case Rectangle r when r.Length == 0 || r.Height == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Triangle t:
            return t.Base * t.Height * 2;
        case Rectangle r:
            return r.Length * r.Height;
        case null:
            throw new ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null");
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

null パターンの特殊なケースに注目します。定数 null は、型がありませんが、任意の参照型または null 許容型に変換できるためです。

まとめ

"パターン マッチング コンストラクト" を使用すると、継承階層で関連付けられていないさまざまな変数および型の間の制御フローを簡単に管理できます。 また、ロジックを制御して、変数でテストする任意の条件を使用することもできます。 これにより、構築する分散アプリケーションが増えるにつれてより頻繁に必要になるパターンと表現形式が実現します。分散アプリケーションでは、データと、そのデータを操作するメソッドが分離されています。 このサンプルで使用されている図形の構造体にメソッドは含まれていません。含まれているのは、読み込み専用のプロパティのみです。 パターン マッチングは、あらゆるデータ型で使用できます。 オブジェクトを調査する式を記述し、それらの条件に基づいて制御フローを決定します。

このサンプルのコードを、抽象的な Shape と特定の派生図形のクラス階層を作成し、それぞれに面積を計算するための仮想メソッドが独自に実装されている場合の設計と比較してください。 一般に、パターン マッチング式は、データを扱う際にデータ ストレージの問題と動作の問題を分離したい場合に非常に便利なツールであることがわかります。