やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

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。少ない順に優先して使うものと覚えればいいかも?

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux