概要
前項で説明した通り、C# 7.0で、is
演算子とswtich
ステートメントが拡張されて、is
/case
の後ろにパターンを書けるようになりました。
パターンには以下のようなものがあります。
パターン | バージョン | 概要 | 例 |
---|---|---|---|
型パターン | C# 7.0 | 型の判定 | int i 、string s |
定数パターン | C# 7.0 | 定数との比較 | null 、1 |
var パターン | C# 7.0 | 何にでもマッチ・変数で受け取り | var x |
破棄パターン | C# 8.0 | 何にでもマッチ・無視 | _ |
位置パターン | C# 8.0 | 分解と同じ要領で、再帰的にマッチングする | (1, var i, _) |
プロパティ パターン | C# 8.0 | プロパティに対して再帰的にマッチングする | { A: 1, B: var i } |
パターンの組み合わせ | C# 9.0 | and や or などでパターンの組み合わせができる |
int x and (x is 0 or 1) |
関係演算パターン | C# 9.0 | < や > などで数値の範囲を指定してマッチングする |
<= 0 and < 10 |
リスト パターン | C# 11.0 | 配列やリストなどにマッチ | [] , [_, ..] |
サンプル コード: https://github.com/ufcpp/UfcppSample/tree/master/Chapters/Data/Patterns
非再帰パターン
Ver. 7.0
C# の文法上の区別する意味はないんですが、 パターンのうち、C# 7.0 で入ったものと 8.0 で入ったものの一番の差は再帰があるかどうかです。 C# 7.0 からあるパターンは1層限り、8.0 で追加されたパターンは再帰的に何層もマッチできます。 (再帰がある方が難しいので後からの追加になりました。)
ここではまず、文法が簡単な再帰のないパターンから説明していきます。
型パターン (宣言パターン)
C# 6.0以前から元々あった is
演算子の自然な拡張になっているのが型パターン(type pattern)です。
以下のように、型の後ろに続けて、マッチした結果を変数で受け取れます。
static void M(object x)
{
if (x is int i) Console.WriteLine("int " + i);
else if (x is string s) Console.WriteLine("string " + s);
}
is
や case
の後ろで変数宣言をしているような形なので、宣言パターン(declaration pattern)とも呼びます。
(というか、C# 8.0以降は宣言パターンの方が正式な呼び方に変わっていそうです。)
型パターンは、旧来からある is
演算子や as
演算子とほぼ同じ挙動です。
上記の例は、概ね以下のコードと同じ動作になります。
if (x is int)
{
var i = (int)x;
Console.WriteLine("int " + i);
}
else
{
string s = x as string;
if (s != null)
{
Console.WriteLine("string " + s);
}
}
as
+ != null
になっていることからわかる通り、
型パターンは null にはマッチしません。
(以下のように、たとえ変数の型が一致していたとしても、null にはマッチしません。)
static void Main()
{
M("abc"); // matched abc
M(null); // 何も表示されない
}
static void M(string x)
{
if (x is string s) Console.WriteLine("matched " + s);
}
型パターンの簡単化
Ver. 9.0
C# 9.0 で型パターンがちょっとだけシンプルになりました。
型パターンは元々 C# 1.0 からある is
演算子の延長として作られています。
ところが、is
の場合は x is T
と書けるのに、switch
では T _
のように変数宣言か _
(破棄) を伴う必要がありました。
これが C# 9.0 で改善されています。
int Is(object x)
{
if (x is string)
{
return 1;
}
return 0;
}
int Switch(object x)
{
switch (x)
{
// C# 8.0 までは string _ と書く必要あり
case string: return 1;
}
return 0;
}
int SwitchExpr(object x) => x switch
{
// C# 8.0 までは string _ と書く必要あり
string => 1,
_ => 0,
};
C# 9.0 時点でこれが書けたなかったのは次節の定数パターンとの混同を避けるためです。
例えば C# 9.0 では以下のようなコードが書けます。
こんなコードを書くこと自体少ないと思いますが、is
の場合とswitch
の場合で、型と定数、どちらが優先されるかが違うので注意が必要です。
class X { }
class Program1
{
static int M(object x) => x switch
{
X => 1, // これは x の型がクラス X
_ => 0,
};
}
class Program2
{
const int X = 1;
static int M1(object x) => x switch
{
X => 1, // これは定数 1
_ => 0,
};
static bool M2(object x) => x is X; // でもこれはクラス X (C# 8.0 以前との互換性のため)
}
定数パターン
is
やcase
の後ろには定数も書けます。これを定数パターン(constant pattern)と言います。
単体で見ると普通に ==
を使えば済むことも多いわけですが、
定数パターンであれば他のパターンとの混在ができます。
switch (x)
{
// 定数パターン
case 0: return 0;
// 型パターン
case string s: return s.Length;
default: return -1;
}
名前通り定数しか使えません。
変数との値比較がしたければ、when
句を使うなどが必要です。
static int M(object x, int comparand)
{
switch (x)
{
// case comparand: とは書けない。
// 型パターン + when 句を使う。
case int i when i == comparand: return 0;
default: return -1;
}
}
ちなみに、定数パターンでは、ユーザー定義演算子を見ません。
以下のように、==
とis
で挙動が違う場合があります。
using System;
class X
{
// 全てのインスタンスが等しいという挙動。
// 当然、x == null も常に true。
public static bool operator ==(X a, X b) => true;
public static bool operator !=(X a, X b) => false;
}
class Program
{
static void Main()
{
var x = new X();
// なんでも true なので、== null も true
Console.WriteLine(x == null);
// ユーザー定義の == は見ない。x が本当に null かどうかを見て、false になる
Console.WriteLine(x is null);
}
}
ポインターの null 比較
Ver. 8.0
細かい修正ですが、C# 8.0 からポインターに対してもパターン マッチングが使えるようになりました。
といってもプロパティや Deconstruct
メソッドを持っているわけではないので、実質的には is null
チェック用です。
static unsafe void M(int* p)
{
// 元々 OK。
Console.WriteLine(p == null);
// C# 8.0 から OK。
Console.WriteLine(p is null);
}
ReadOnlySpan に対するパターンマッチ
Ver. 11
C# 11 で、ReadOnlySpan<char>
に対して文字列リテラルによる定数パターンが使えるようになりました。
// string を渡せたところには ReadOnlySpan<char> を渡せるように。 ReadOnlySpan<char> s = Console.ReadLine(); // is も if (s is "a") { } // switch ステートメントも switch (s) { case "b": break; } // switch 式も OK。 var x = s switch { "c" => 1, _ => 2, };
文字列処理に対して ReadOnlySpan<char>
を使う機会が多くなってきたので特殊対応したそうです。
(パターンに書かれているのは ""
みたいな「定数」ですが、
そこに string
から ReadOnlySpan<char>
の変換が挟まっていて定数とは言い切れない状態です。
C# チーム自身はそれほど実装に乗り気ではなく、外部からのコントリビューションで実装された機能になります。)
var パターン
型パターンと似ていますが、具体的な型名の代わりに var
キーワードを使うと、
任意の型にマッチするパターンになります。
これを var パターン (var pattern)と言います。
switch
の最後に書いて「その他全部」な分岐に使ったりします。
static int M(object x)
{
switch(x)
{
case 0: return 0;
case string s: return s.Length;
case var other: return other.GetHashCode();
// あるいは、変数で受け取る必要がないときは _ にしておけば破棄の意味なる
// case var _:
}
}
あと、少し悪用気味ではありますが、式中での変数宣言に使えたりします。
while (Console.ReadLine() is var line && !string.IsNullOrEmpty(line))
{
Console.WriteLine(line);
}
1つ注意が必要な点として、var パターンは型パターンと違って、null にもマッチします。
string s = null;
Console.WriteLine(s is string x); // false
Console.WriteLine(s is var y); // true
null をはじきたい場合は、var ではなく、後述するプロパティ パターンを使ってx is {} nonNull
と書いたりします。
破棄パターン
Ver. 8.0
何にでもマッチして、マッチ結果を受け取る必要がない場合、_
を使って値を破棄できます。これを破棄パターン(discard pattern)と言います。
再帰はしないんですが、switch
式の中と、再帰パターン内でしか使えないので C# 8.0 での実装になります。
is
やステートメントの方のswitch
のcase
の後ろではvar _
と書く必要がありますが、switch
式の場合は_
だけで値を破棄します。
static int M(object x)
=> x switch
{
0 => 0,
string s => s.Length,
_ => -1
};
余談: 破棄パターンが C# 8.0 からな理由
ちなみに、is
や switch
ステートメント内で _
だけでの値の破棄ができないのは既存コードとの互換性のためです。
普通書かないようなコードですが、一応、以下のようなコードが元々合法なため、意味を変えることができませんでした。
using System;
class _Type
{
class _ { }
static void M(object x)
{
Console.WriteLine(x is _); // class _ とのマッチ
}
}
class _Constant
{
const int _ = 0;
static void M(object x)
{
switch (x)
{
case _: // 定数 _ とのマッチ
break;
}
}
}
(あまりにも紛らわしいので、このコードを C# 8.0 でコンパイルすると警告が出ます。)