パターンの組み合わせ

Ver. 9.0

C# 9.0 で andor などのキーワードを使ってパターンの組み合わせ(pattern combinators)ができるようになりました。

  • and: 論理積パターン (conjunctive patterns)。両辺に書いたパターンの両方にマッチすることを求める
  • or: 論理和パターン (disjunctive patterns)。両辺に書いたパターンの少なくとも一方にマッチすることを求める
  • not: 否定パターン (negated patterns)。後ろに書いたパターンの否定を取る
  • (): 括弧付きパターン (parenthesized patterns)。and, or などの結合優先度を指定するためにパターンを () でくくる

and パターン

2つのパターンを and キーワードでつなぐことで、両方のパターンにマッチしたときだけマッチした扱いになります。 (論理積パターン(conjunctive patterns)と言ったりもします。)

例えば、複数のインターフェイスをすべて実装しているかを判定するとかに使えます。

int M(object x) => x switch
{
    // 2つのインターフェイスを両方実装している場合にマッチ。
    // この時、パターン中で宣言した a, b にはちゃんと両方「初期化済み」判定を受ける。
    IA a and IB b => a.A * b.B,
    _ => 0,
};
 
interface IA { int A { get; } }
interface IB { int B { get; } }

その他、後述する関係演算パターンと組み合わせて、「0~10まで」みたいな数値の範囲を表すことができます。

int M(byte x) => x switch
{
    >= 0 and < 10 => 0,
    >= 10 and < 100 => 1,
    >= 100 => 2,
};

or パターン

2つのパターンを or キーワードでつなぐことで、少なくともいずれか片方のパターンにマッチしたときにマッチした扱いになります。 (論理和パターン(disjunctive patterns)と言ったりもします。)

単純に複数の値にマッチさせたり、複数の型にマッチさせることができます。

bool IsSmallPrime(int x) => x is 2 or 3 or 5 or 7;
 
bool IsTrue(bool? x) => x switch
{
    true => true,
    // _ (true 以外)と差はないものの、あり得る値を網羅していることがチェックできるという点で
    // true, false, null の3つの値を並べる意味はなくはない。
    false or null => false,
};

また、複数の型にマッチさせたりもできます。

bool IsByte(object x) => x is byte or sbyte;

and と同様、後述する関係演算パターンとの組み合わせでも使えます。

int Triangular(int x) => x switch
{
    < -1 or > 1 => 0,
    _ => 1 - Math.Abs(x),
};

文脈キーワードの and, or

C# のキーワード追加では恒例行事ですが、 既存コードをなるべく壊さないように、後付けな andor などは文脈キーワードになっています。

例えば、あまり意味のあるコードではないものの以下のようなコードは有効な C# コードになります。

// 水色の部分は型名の or, and。青色の部分はキーワードの or, and。
bool M(object x) => x is or or and and and;
 
class and { }
class or { }

not パターン

パターンの前に not キーワードを置くことで、元のパターンの成否を反転させることができます。 (否定パターン(negated patterns)と言ったりもします。)

おそらく一番使い道があるのは not null だと思います。

using System;
 
#nullable enable
 
void M(string? s)
{
    if (s is not null)
    {
        Console.WriteLine(s.Length);
    }
}

string 相手だと x != null と大差ないですが、場合によってはパフォーマンスがよくなることもあります。 また、! の視認性があまりよくないので != よりも is not の方を好む人もいるようです。

あと、いわゆる early return に使えます。 以下のように、特定条件を満たさないときに早々に return ステートメントで関数を抜けてしまうときに not パターンが使えます。

using System;
 
void PositivePattern(object x)
{
    if (x is string s)
    {
        Console.WriteLine(s.Length);
    }
}

// ↑のメソッドを early return で書き直したもの。
void EarlyReturn(object x)
{
    // if の中に限り、not + 型パターンで変数宣言可能。
    if (x is not string s) return;
 
    // この場合、if 中(not string の時) には s が使えず、
    // その後ろ(string の時)でだけ s が使える。
 
    Console.WriteLine(s.Length);
}

括弧付きパターン

not, and, or の結合順位は !, &&, || と同じで、notandor の順です。

例えば以下のような書き方をすると、and の結合が優先されます。

bool IsAsciiLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

&&|| でもよくある話ですが、優先度がわかりにくくて読むときにつらかったりします。 また、or の方を優先したいことも当然あります。

そこで、パターンを () で囲んで結合優先度を明示することができるようになりました。 (括弧付きパターン(parenthesized patterns)と言ったりもします。) 先ほどの IsAsciiLetter の例は以下のようにも書けます。

// () を付けて優先度を明示。
bool IsAsciiLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

前述の「複数のインターフェイスをすべて実装しているかを判定」と「not パターンを使った early return」の組み合わせもできます。

using System;
 
void M(object x)
{
    if (x is not (IA a and IB b)) return;
 
    // a, b ともに使える。
    Console.WriteLine(a.A * b.B);
}
 
interface IA { int A { get; } }
interface IB { int B { get; } }

関係演算パターン

Ver. 9.0

<, <=, >, >= の4つの関係演算子を使って数値の大小をパターンの中に書けます。 (関係演算パターン(relational patterns)と言ったりします。)

int M(byte x) => x switch
{
    < 10 => 1, // 0~9
    >= 10 and <= 99 => 2, // 10~99
    > 99 => 3, // 100~255
};

初期の案では、C# 8.0 で範囲アクセス用に .. 演算子を導入したのに対して、「範囲パターン」も用意したいというものでした。 ただ、x..y みたいな範囲パターンだと、両端(この場合 xy)を含むかどうかがわかりにくくて困るだろうということで不採用になっていました。 ( .. 演算子はインデックス用途に絞ったことで、先頭xは含む、末尾yは含まないというルールにできましたが、「範囲パターン」の場合はあまり用途を絞れないので同じルールだと使いにくいという問題があります。)

他のプログラミング言語だと、範囲を表すために <.., =.., ..<, ..= など .. の前後に <= を付けることで両端の含む・含まない問題を解決していたりします。 しかし、C# ではもういっそ、<, <=, >, >=and パターンの組み合わせで範囲を表そうということになりました。

更新履歴

ブログ