switch 式

Ver. 8.0

C# 8.0 では、switch版が追加されました。 式なので戻り値が必須ですが、どこにでも書けて便利です。 また、従来の switch ステートメントは C# の前身となるC言語のものの名残を強く残し過ぎていて使いにくいものでしたが、その辺りも解消されて使いやすくなりました。

例えば、以下のような列挙型を使った分岐を考えてみます。

using static 年号;
 
enum 年号
{
    明治, 大正, 昭和, 平成
}

これまでだと、以下のような書き方をせざるを得ないことがあったかと思います。

public void M(年号 e)
{
    int y;
    switch (e)
    {
        case 明治:
            y = 45;
            break;
        case 大正:
            y = 15;
            break;
        case 昭和:
            y = 64;
            break;
        case 平成:
            y = 31;
            break;
        default: throw new InvalidOperationException();
    }
    // y を使って何か
}

こういう書き方は結構しんどいわけですが、しんどい理由は以下のような点にあります。

  • それぞれの条件で1つずつ値を返したいだけなのにステートメントを求められる
  • break が必須
  • case ラベルもうざい

ちょこっとごまかす方法として、以下のように別メソッドを1段挟む方法もあるにはありますが、相変わらずcasereturnがうっとおしいです。

public void M(年号 e)
{
    int lastYear()
    {
        switch (e)
        {
            case 明治: return 45;
            case 大正: return 15;
            case 昭和: return 64;
            case 平成: return 31;
            default: throw new InvalidOperationException();
        }
    }
 
    var y = lastYear();
    // y を使って何か
}

これは、C# 8.0 の switch 式を使うと、以下のように書き直すことができます。

public void M(年号 e)
{
    var y = e switch
    {
        明治 => 45,
        大正 => 15,
        昭和 => 64,
        平成 => 31,
        _ => throw new InvalidOperationException()
    };
    // y を使って何か
}

文法的には以下のようになります。

変数 switch
{
    パターン1 => 式1,
    パターン2 => 式2,
      ・
      ・
      ・
}

ステートメントの方のswitchとの弁別のために、switchキーワードは後置きになっています。

最後の1個のコンマはあってもなくてもかまいません。 配列オブジェクト初期化子、コレクション初期化子と同様です。

パターンの部分には「パターン マッチング」で説明している任意のパターンを書けます。 また、whenを付けることもできます。

static int M(object obj) => obj switch
{
    int x when x > 0 => 1,
    int _ => 2,
    _ => 3,
};

網羅性

式であるからには、switch 式は必ず値を返す必要があります。 なので、パターンには網羅性(exhaustiveness)が求められます。 すなわち、「どのパターンも満たさずswitch式を抜けてしまう」みたいな状態は許容されません。 ちゃんと C# コンパイラーが網羅性をチェックしていて、抜けがあるとコンパイル エラーになります。

多くの場合、末尾にvarパターン破棄パターンを書いて漏れを防ぎます。

static int M(int x) => x switch
{
    1 => 2,
    2 => 4,
    _ => 8, // 破棄パターンで「残り全部」を受付
};
 
static int M(object x) => x switch
{
    int i => i,
    string s => s.Length,
    var other => other.GetHashCode(), // var パターンで「残り全部」を受付
};

今のところ、boolだけは網羅性を確実にチェックできます。

static int M(bool x) => x switch
{
    true => 1,
    false => 0,
    // true/false で全パターン網羅できているので _ とかは不要
};
 
static int M(bool x, bool y) => (x, y) switch
{
    (false, false) => 0,
    (true, false) => 1,
    (false, true) => 2,
    (true, true) => 4,
    // 上記4パターンしかありえないので _ とかは不要
};

将来的には、enum型の網羅性や、派生クラスの網羅性もチェックしたいそうですが、 「後からのメンバー追加に弱くなる」など課題があるため、実装されるかどうかは不明瞭です。

余談: bool の網羅性

前節のswitch式の網羅性チェックと関連して、ステートメントの方のswitchでも、boolの網羅性チェックが働くようになりました。 C# 8.0 前後で挙動が変わるのでご注意ください。

すなわち、以下のようなswitchステートメントを書いたとき、default句に関する扱いが変わります。

static int M(bool b)
{
    switch (b)
    {
        case false: return 0;
        case true: return 1;
        default: return -1;
    }
}
  • C# 7.3 以前: default が必須
  • C# 8.0 以降: default が要らないというか、むしろ書くと警告(絶対に来ない条件があるという扱い)

C# 7.3 以前がどうしてそうなっていたかは以前ブログを書いたのでそちらを参照してください: 「bool 型の false, true, それ以外」。

更新履歴

ブログ