C#8.0パターンマッチにおけるベストプラクティスを考えてみた
できるだけ再帰パターンである位置、プロパティの2パターンを使えばいい感じ。
成果物
背景
C#8.0のパターンマッチはできることが多い。どういうとき、どう書くのがベストなのかわからない。大まかな方針だけでも掴みたいので考えてみた。
一覧
ベストプラクティス | 理由 |
---|---|
switch 式を使うべし(switch 文でなく) |
case , break , default などを省略できる。 |
型は1つに絞るべし | 名前を省略できる(位置・プロパティパターンで) |
プロパティパターンを使うべし(when 句でなく) |
条件の重複チェックしてくれる |
switch式において、マッチした値のメンバを参照するか否かによって最適なパターンが変わる。
メンバ参照 | ベストプラクティス | 理由 |
---|---|---|
する | プロパティパターンの非nullマッチを使うべし(varパターンでなく) | varはnull にもマッチするから(NullReferenceException 例外発生しうる) |
しない | 破棄パターンを使うべし | _ で参照不可になるから |
1. switch
式を使うべし(switch
文でなく)
switch文
switch (x) { case 0: Console.WriteLine("Case 0"); break; case 1: Console.WriteLine("Case 1"); break; default: Console.WriteLine("Case default"); break; }
switch式
string M(object x) => x switch { 0 => "Case 0", 1 => "Case 1", _ => "Case default", } Console.WriteLine(M(x));
圧倒的スマート!
2. 型は1つに絞るべし
絞れるときは絞ったほうがいい。型の名前を省略できるから。
before
int Bad(object x) x switch => { Point(1, 2) => 0, _ => -1, };
after
int Best(Point p) p switch => { (1, 2) => 0, _ => -1, };
3. プロパティパターンを使うべし(when
句でなく)
条件が重複しているとき、コンパイルエラーで通知してくれるから。
when句
int M1(object obj) => obj switch { string s when s.Length == 0 => 0, string s when s.Length == 0 => 1, // 条件重複! _ => -1, };
プロパティパターン
int M2(object obj) => obj switch { string { Length: 0 } => 0, string { Length: 0 } => 1, // 条件重複! コンパイルエラーで通知してくれる _ => -1, };
error CS8510: このパターンは、switch 式の前の arm で既に処理されています。
コピペしたまま修正忘れたときとか、こうなりそう。もしエラーにしてくれたらコンパイルの時点でミスに気付ける。だが、エラーにしてくれなければ、単体テストで失敗するまで気付けない。
4. マッチ値のメンバ参照
switch式において、マッチした値のメンバを参照するか否かによって最適なパターンが変わる。
メンバ参照 | ベストプラクティス | 理由 |
---|---|---|
する | プロパティパターンの非nullマッチを使うべし(varパターンでなく) | varはnull にもマッチするから(NullReferenceException 例外発生しうる) |
しない | 破棄パターンを使うべし | _ で参照不可になるから |
4-1. マッチ値のメンバを参照しないなら、破棄パターンを使うべし
varパターン
string M(object x) => x switch { 0 => "Case 0", 1 => "Case 1", var other => "Case default", } Console.WriteLine(M(x));
破棄パターン
string M(object x) => x switch { 0 => "Case 0", 1 => "Case 1", _ => "Case default", } Console.WriteLine(M(x));
破棄パターンのほうがスマート。
だが、メンバ参照はできない。以下のようなエラーとなる。
string M(object x) => x switch { 0 => "Case 0", 1 => "Case 1", _ => _.ToString(), }; Console.WriteLine(M(2));
error CS0103: 現在のコンテキストに '_' という名前は存在しません。
4-2. マッチ値のメンバを参照するなら、プロパティパターンを使うべし(varパターンでなく)
メンバ参照するときは以下の条件が必須である。
- 変数が必要
- 非nullであること
プロパティパターン
string Best(Point p) => p switch { { } nonNull => nonNull.ToString(), null => "null", }; Console.WriteLine(Best(new Point(1,2)));
varパターン
string Bad(Point p) => p switch { null => "null", var other => other.ToString(), }; Console.WriteLine(Bad(new Point(1,2)));
上記は結果的に同じ。ただし、varパターンのvar other
にはnull
も入りうる。null
パターンチェックと重複してしまう。なので、もし間違ってnull
パターンを後ろに書いてしまうと、条件重複コンパイルエラーになる。
string Bad2(Point p) => p switch { var other => other.ToString(), null => "null", }; Console.WriteLine(Bad2(null));
error CS8510: このパターンは、switch 式の前の arm で既に処理されています。
NullReferenceException
が発生しるう事態にはならないので、そこは安心。だが、順序が変わっただけでコンパイルエラーになるのはウザい。将来コードをメンテナンスするとき面倒。なのでvarパターンよりプロパティパターンのほうが良い。
つまり_
がダメなら{ }
。var
はいらない子。それぞれ文字数が1, 2, 3。少ない順に優先して使うものと覚えればいいかも?
対象環境
- Raspbierry pi 3 Model B+
- Raspbian stretch 9.0 2018-11-13 ※
- bash 4.4.12(1)-release ※
- SQLite 3.29.0 ※
- C# dotnet 3.0.100
$ uname -a Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux