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段挟む方法もあるにはありますが、相変わらずcase
やreturn
がうっとおしいです。
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
式の優先度は単項演算の下、乗除演算の上になります。
++x
や await x
は switch
式よりも先に評価されて、
x * y
や x + y
は switch
式よりも後に評価されます。
// これは (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
式では、いろいろな条件でいろいろな値を返すわけですが、
値から「共通の型」を決定できない場合があります。
例えば、以下のように、(例え同じクラスから派生していたとしても)異なる型 A
と B
の「共通の型」は判定できず、
コンパイル エラーを起こします。
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 };
}