Menu

C#のパターンマッチング:is、switchのパターン、プロパティパターン、リストパターン

C#のパターンマッチングの仕組みを解説します。型パターンと定数パターンを使うis演算子、caseのパターンとwhenを使うswitch文、switch式、プロパティ、タプル、位置指定のパターン、関係パターンと論理パターン(and、or、not)、リストパターン、そしてそれぞれを追加したC#のバージョンを扱います。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

パターンマッチングは値を形と照合します。「Circle か」「nullか」「Status が "paid" で Total が100を超えているか」といった具合です。判定が成功すると、パターンは値の一部を新しい変数に取り出すこともできます。C# 7で is 演算子から小さく始まり、それ以降のすべてのバージョンで拡張されてきたので、このページではそれぞれの形のバージョンを記しています。

isの型パターン(C# 7)

C# 7より前は、型を確認して使うのに2ステップが必要でした。as とnullチェック、または is とキャストです。型パターンは両方を一度に行い、型付きの変数を与えます。

出力:

card ending 4242: 49.90
transfer from PT50...: 1200
no payment
other payment: 5

p is CardPayment card が真になるのは p がnullでない CardPayment(またはそこから派生したクラス)の場合だけで、そのとき card は正しい静的な型でそれを保持します。null値は決して型パターンに一致しないので、null の支払いはnullチェックまで進みます。

スコープと確実な代入

パターンの変数は外側のブロックに存在しますが、コンパイラがそれを読むことを許すのは、一致が成功したとわかっている場所だけです。そのため、早めに抜けて後から変数を使う「ガード節」の形が動きます。

出力:

19.99
42 is over 40
price must be text

boxed is int n && n > 40 は、もう1つのよくある形を示しています。&& の右辺は一致が成功したときにしか実行されないので、そこでは変数を使えます。|| の右辺では代入されていないので、コンパイラがそう指摘します(CS0165)。

パターンはボックス化の解除も行います。boxed is int n はボックス化された int に対して成功し、例外を投げうるキャストなしで値を与えます。null許容値型では、maybe is int n は maybe が値を持つときにちょうど成功します。

定数パターンとnullパターン

定数もパターンです。最もよく使われるのは x is null で、C# 7では x is 0、status is "paid"、enumのメンバーも使えます。

if (customer is null) return;          // null check that ignores any == overload
if (count is 0) Console.WriteLine("empty");
if (level is LogLevel.Error) Alert();

型の演算子のオーバーロードで結果が変わることがないので、多くのチームは == null より is null を好みます。C# 9では否定形の is not null が追加されました。

switch文のパターン(C# 7)

同じ型パターンを case のラベルにでき、when 句で条件を加えられます。caseは上から下へ試されるので、限定的なものから一般的なものの順に並べます。

static string Describe(object o)
{
    switch (o)
    {
        case null:                           return "nothing";
        case int n when n < 0:               return "negative number";
        case int n:                          return $"number {n}";
        case string s when s.Length == 0:    return "empty text";
        case string s:                       return $"text of {s.Length} chars";
        case IEnumerable<int> list:          return "a sequence of numbers";
        default:                             return o.GetType().Name;
    }
}

Describe(-4);                 // negative number
Describe("");                 // empty text
Describe(new List<int>());    // a sequence of numbers
Describe(2.5);                // Double

コンパイラは順序を確認します。前のcaseが網羅しているために決して一致しないcaseはエラーCS8120です。default は、どこに書かれていても常に最後に評価されます。上の支払いの例のように if/is の連鎖で同じ論理を書く方法は、C# 7以降のすべてのバージョンで動きます。

switch式(C# 8)

switch式は、「いくつかのケースから1つの値を計算する」ための簡潔な形です。値が先に来て、次に switch、そしてカンマで区切った pattern => result のアームが続き、_ がすべてを受け止めます。

public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;

static double Area(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle { Width: var w, Height: var h } => w * h,
    _ => throw new ArgumentException("Unknown shape", nameof(shape)),
};

Area(new Rectangle(3, 4));   // 12

実行時にどのアームも一致しないと SwitchExpressionException が投げられ、処理されない入力があるとコンパイラが判断できれば警告(CS8509)を出します。構文そのものはswitch式のページで扱います。このページの残りは、アームに書けるパターンについてです。

プロパティパターン(C# 8、C# 10で拡張)

プロパティパターンは、オブジェクトのプロパティを、波かっこの中の入れ子のパターンと照合します。オブジェクトはnullでない必要もあるので、{ } だけなら「nullでない」を意味します。

public record Address(string City, string Country);
public record Order(decimal Total, string Status, Address ShipTo, int Items);

static string Shipping(Order order) => order switch
{
    { Status: "cancelled" }                   => "no shipment",
    { ShipTo.Country: "PT", Total: >= 50m }   => "free, domestic",     // C# 10 dotted form
    { ShipTo.Country: "PT" }                  => "4.90, domestic",
    { Total: > 200m }                         => "free, international",
    _                                         => "12.00, international",
};

Shipping(new Order(60m, "paid", new Address("Porto", "PT"), 2));    // free, domestic
Shipping(new Order(260m, "paid", new Address("Lyon", "FR"), 2));    // free, international

C# 8と9では入れ子の形が必要です:{ ShipTo: { Country: "PT" } }。C# 10では、上で使っているドットの省略形が許されます。プロパティパターンは is でも使えるので、複数の条件の確認が説明文のように読めます:if (order is { Status: "paid", Items: > 0 })。

関係パターンと論理パターン(C# 9)

C# 9では比較(<、<=、>、>=)と組み合わせ子の and、or、not が追加され、範囲をパターンにできるようになりました。

static string Grade(int score) => score switch
{
    < 0 or > 100      => "invalid",
    >= 90             => "A",
    >= 75 and < 90    => "B",
    >= 50             => "C",
    _                 => "F",
};

Grade(82);    // B
Grade(101);   // invalid

if (input is not null and not "") { ... }
if (c is >= 'a' and <= 'z' or >= 'A' and <= 'Z') { ... }   // and binds tighter than or

C# 7での同等のものは、if/else if の連鎖か、when 句を使う switch(case int s when s >= 90:)です。

タプルパターンと位置指定のパターン(C# 8)

複数の値で一度に分岐するには、タプルでswitchします。

static string Quadrant(int x, int y) => (x, y) switch
{
    (0, 0)       => "origin",
    (> 0, > 0)   => "I",
    (< 0, > 0)   => "II",
    (< 0, < 0)   => "III",
    (> 0, < 0)   => "IV",
    _            => "on an axis",
};

Quadrant(-2, 5);   // II

位置指定のパターンは、レコードを含め、Deconstruct メソッドを持つ任意の型に対して同じことを行います:case Point(0, 0): や p is Rectangle(var w, var h) && w == h です。

リストパターン(C# 11)

リストパターンは、配列やリストをその要素で照合します。.. は任意の数の要素(「スライス」)に一致し、各要素の位置には他の任意のパターンを書けます。

static string Route(string[] args) => args switch
{
    []                         => "help",
    ["add", var item]          => $"add {item}",
    ["remove", var item, ..]   => $"remove {item}",
    [var cmd, ..]              => $"unknown command {cmd}",
};

Route(new[] { "add", "milk" });             // add milk
Route(new[] { "remove", "milk", "now" });   // remove milk

int[] nums = { 1, 2, 3, 4 };
if (nums is [1, .., var last]) Console.WriteLine(last);   // 4

どのバージョンで何が追加されたか

パターン例バージョン
変数付きの型パターンo is Circle cC# 7.0
定数パターンx is null、n is 0C# 7.0
var パターンx is var vC# 7.0
case ラベルのパターン、when のガードcase int n when n < 0:C# 7.0
switch式、破棄 _x switch { ... }C# 8
プロパティパターン{ Status: "paid" }C# 8
タプルパターン(x, y) switch { (0, 0) => ... }C# 8
位置指定のパターンPoint(0, 0)C# 8
関係パターン> 100、<= 0C# 9
論理パターンnot null、>= 1 and <= 5、'a' or 'b'C# 9
変数なしの型パターンcase Circle:、Circle => ...C# 9
拡張プロパティパターン{ ShipTo.City: "Porto" }C# 10
リストパターンとスライス[first, .., last]C# 11

よくある間違い

  • caseを一般的なものから限定的なものの順に並べる。 case CardPayment c: の前に case Payment p: を置くと、後者が到達できなくなります(CS8120)。限定的なものを先にします。
  • 型パターンがnullに一致すると思う。 null is string s はfalseです。null は独自のcaseか is null で扱います。
  • 一致が失敗したかもしれない場所でパターンの変数を読む。 a is int n || ... の後では、n は代入されていません(CS0165)。
  • 仮想メソッドの代わりに長い型のswitchを使う。 新しいサブクラスのたびに5か所の同じswitchを編集するなら、その振る舞いはクラスの階層に属します。パターンが真価を発揮するのは、外部からのデータ(JSONの形、メッセージ、入力のタプル)と、自分が管理する閉じた型の集合です。
  • switch式ですべてを受け止めるアームを忘れる。 一致しない入力は実行時に例外を投げます。警告CS8509を読みます。

よくある質問

C#のパターンマッチングとは何ですか?

パターンマッチングは値を形と照合し、一致すれば同じステップでその一部を変数に取り出せます。if (shape is Circle c) は型を判定し、型付きの c を与えます。order is { Status: "paid", Total: > 100 } はプロパティを判定します。パターンは is 式、switch 文、switch式に現れます。

C#で変数付きのisはどう使いますか?

if (obj is Customer c) { ... } は、obj が(nullでない)Customer であることを確認し、if の中で Customer 型の c に代入します。古い var c = obj as Customer; if (c != null) の組を置き換えます。変数が確実に代入されるのは判定が真とわかっている場所だけなので、if (!(obj is Customer c)) return; なら if の後でも c を使えます。

C#のis nullと== nullの違いは何ですか?

x is null は常に参照そのものを確認します。x == null は、型が == をオーバーロードしていればその演算子を呼び、通常は同じ答えになりますが保証はありません。逆の判定は、x != null、!(x is null)、C# 9以降は x is not null と書きます。

whenを使うswitchのパターンマッチングはどう動きますか?

case ラベルには、パターンに加えて when のガードを書けます:case Order o when o.Total > 100:。caseが一致するのは、パターンが一致し、かつ条件が真のときだけです。caseは上から下へ試されるので、より限定的なものを先に置きます。前のcaseがすでに網羅しているために到達できないcaseがあると、コンパイラはCS8120を報告します。

どのC#のバージョンでどのパターンが追加されましたか?

C# 7.0:is と case の型、定数、var のパターン、そして when。C# 8:switch式、プロパティ、タプル、位置指定のパターン。C# 9:関係(> 5)、論理(and、or、not)、変数なしの型パターン。C# 10:拡張プロパティパターン({ Address.City: "Porto" })。C# 11:リストパターン([1, .., var last])。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める