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 式の優先度

switch 式の優先度は単項演算の下、乗除演算の上になります。 ++xawait xswitch 式よりも先に評価されて、 x * yx + yswitch 式よりも後に評価されます。

// これは (await b) switch { ... } の意味になって、
// bool を await できないのでコンパイル エラー。
static async Task M1(bool b, Task x, Task y)
    => await b switch { true => x, false => y };
 
// これは (++x) switch { ... } の意味で、
// x に -1 を渡した時だけ false に。
static bool M2(int x)
    => ++x switch { 0 => false, _ => true };
 
// これは y * (switch { ... }) の意味で、
// 0 か y が返る。
static int M2(int x, int y)
    => y * x switch { 0 => 0, _ => 1 };

網羅性

式であるからには、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, それ以外」。

ターゲットからの型決定

switch 式にはターゲットからの型推論が働きます。

ここでいうターゲットというのは結果を渡す先のことで、例えば以下のような書き方をした場合、 null を渡す先が int? 型の変数なので、この int? が「ターゲットの型」になります。

int? x = null;

switch 式では、いろいろな条件でいろいろな値を返すわけですが、 値から「共通の型」を決定できない場合があります。 例えば、以下のように、(例え同じクラスから派生していたとしても)異なる型 AB の「共通の型」は判定できず、 コンパイル エラーを起こします。

class Base { }
class A : Base { }
class B : Base { }
 
static object M(int i)
{
    // 値が A と B で違う型なので、switch 式が返す型を決定できない。
    // コンパイル エラーになる。
    var x = i switch
    {
        0 => new A(),
        _ => new B(),
    };
 
    return x;
}

これくらいならば Base が共通の型だと判定してほしくも思いますが、 多段派生していたり、インターフェイスも実装していたり複雑な場合のことを考えるとそんなに簡単な話ではありません。

// 型 D と F の「共通型」といわれると何?
// インターフェイス J? それともクラス A?
interface I { }
interface J { }
class A { }
class B : A, I { }
class C : A { }
class D : B, J { }
class E : B { }
class F : C, J { }

この問題の回避策は2つあって、1つは特に難しいこともなく、「キャストしろ」というものです。 C# コンパイラーが理解できるところまでかみ砕いたコードを書いてあげなきゃいけないということで、ちょっと煩雑なコードになります。

// 片方を既定型にキャストしておくことで「共通型は Base」と判定できるようになる
var x = i switch
{
    0 => (Base)new A(),
    _ => new B(),
};

もう1つが本節の主題の「ターゲット型からの型決定」です。 先ほどの例では左辺が var (型推論)なのでコンパイルできませんが、 以下のように、ターゲット側の型を明示することで、switch 式の側の型を Base に決定できます。

// 左辺(Base 型の変数)から switch 式の型を Base に決定。
// コンパイルできるようになる。
Base x = i switch
{
    0 => new A(),
    _ => new B(),
};

特に役立つのは「1 と null」(int? になってほしい)とかでしょう。

static void M(bool b)
{
    // これはコンパイル エラー。1 と null の共通型は C# 8.0 時点では決定できない。
    var x = b switch { true => 1, _ => null };
 
    // これはコンパイルできる。ターゲット型から int? に決定済みなので、1 も null も受け付ける。
    int? y = b switch { true => 1, _ => null };
}

更新履歴

ブログ