コンパイル時検査

パターン マッチングでは、値の網羅性を満たしているかどうかと、書いたパターンが重複していないかをコンパイル時に検査してくれる仕組みがあります。

網羅性チェック

いくつかの型は決まった値しかとりません。例えば bool なら truefalse の2値ですし、 bool? でも true, false, null の3値だけです。 byte も高々256個の値しか持ちません。 型スイッチのページにも書いていますが、パターン マッチングではこれらの値をすべて網羅しているかどうか(exhaustiveness: 網羅性)の検査をしてくれます。

// 無警告
int A(bool x) => x switch
{
    true => 1,
    false => 0,
};
 
// 警告あり(CS8655: 条件に null が足りていない)
int B(bool? x) => x switch
{
    true => 1,
    false => 0,
};
 
// 無警告
int C(bool? x) => x switch
{
    true => 1,
    false or null => 0,
};

また、数値型に対しては、関係演算パターンを使って「100未満」と「100以上」というように相補的に値を網羅しているかを検査できます。 例えば以下のコードには条件漏れがあって警告を起こします。

// 警告を起こす
int M(byte x) => x switch
{
    < 10 => 1,
    >= 10 and < 100 => 2,
    // < 100 と > 100 (どちらも 100 は含まない)しかないので、実は 100 が漏れてる
    > 100 => 3,
};

値パターンや or パターンとの組み合わせでも網羅性の検査がかかります。

// 整数の場合は値パターンとかその or パターン、関係演算パターンの組み合わせでも網羅性検査がかかる
int M(uint x) => x switch
{
    0 or 2 or 4 or 6 or 8 => 0,
    1 or 3 or 5 or 7 or 9 => 1,
    >= 10 => -1, // この行がなかったり、条件が > 10 とかだったりすると警告
};

一般の型に対しても、「null か非 null か」みたいな条件が相補的で、これに対しても網羅性の検査がかかります。

// null か非 null かで網羅性検査がかかっていて、どれか1行でも欠けていると警告
int M(int? x, int? y) => (x, y) switch
{
    (null, null) => 0,
    ({ }, null) => -1,
    (null, { }) => 1,
    ({ } x1, { } y1) => x1.CompareTo(y1),
};

条件の重複チェック

switch ステートメント/switch 式中に絶対に到達できない条件があるとき、 ある程度はコンパイル時に検知してコンパイル エラーにしてもらえます。

パターンを使った switch の条件は上から逐次判定なので、要するに、上の方に下にある条件の上位互換な条件があるとコンパイル エラーになります。

一番わかりやすいのは破棄パターンで、これは「何にでも一致するパターン」なので、その下に何かを書くとエラーになります。

int M(object obj) => obj switch
{
    _ => 0,
    string _ => 1,
};

当然ですが、全く同じ条件が2つ以上ある場合にも、1つ目以外には絶対に到達しないのでエラーになります。

int M(object obj) => obj switch
{
    string s => s.Length,
    string _ => 1,
};

ちなみに、whenだと重複チェックが漏れることがあります。 一方、同じような条件でも、再帰パターンを使うとチェックが働きやすいです。

int M1(object obj) => obj switch
{
    // when 句を使うと「同じ条件」判定ができなくなる。コンパイルできてしまう。
    string s when s.Length == 0 => 0,
    string s when s.Length == 0 => 1,
    _ => -1,
};
 
int M2(object obj) => obj switch
{
    // 同じことを再帰パターンでやるとちゃんと重複チェックが掛かる。2つ目でコンパイル エラーに。
    string { Length: 0 } => 0,
    string { Length: 0 } => 1,
    _ => -1,
};

また、前節の網羅性とも関連して、 全ての値を網羅済みのところの後ろに条件を足しても、その行には絶対に来ないのでエラーにできます。 例えば以下のコードはコンパイル エラーになります。

int M(bool a, bool b) => (a, b) switch
{
    (false, false) => 0,
    (true, false) => 1,
    (false, true) => 2,
    (true, true) => 3,
    // bool の場合上記4つ以外は絶対にないことがわかるので、この行でコンパイル エラーになる。
    _ => 4,
};

更新履歴

test

[雑記]

ブログ