switch式は入力値を結果に対応付けます。C# 8で追加され、すべてのcaseが何かを返すか代入するだけのswitch文という、よくある形を置き換えます。
// C# 8 and later
string StatusText(int code) => code switch
{
200 => "OK",
301 or 302 => "Redirect", // 'or' needs C# 9
404 => "Not Found",
>= 500 => "Server Error", // relational patterns need C# 9
_ => "Unknown"
};
上から下へ読みます。switch の前の値が各アームと順に照合され、パターンが最初に一致したアームが結果を返します。case、break、default のキーワードはなく、_(破棄パターン)が何にでも一致して、最後の受け皿になります。
C# 7では、同じ対応付けは小さなメソッドの中のswitch文になり、各caseで return します。
出力:
200 OK
302 Redirect
404 Not Found
503 Server Error
418 Unknown
式の形が短いのは、主に各アームが1行で済み、メソッドの本体がなくなるからです。また式なので、引数、補間の穴、return、フィールドの初期化子など、値を置ける場所ならどこにでも書けます。
構文のルール
var result = input switch
{
pattern1 => value1,
pattern2 when condition => value2,
_ => fallback
};
- 入力が先で、その後に
switchが来ます。文の形とは逆です。 - 各アームは
pattern => expressionです。右辺は1つの式でなければなりません。処理が多いならメソッドを呼びます。 - アームはカンマで区切ります。最後のアームの後の末尾のカンマも許されます。
- 全体が式なので、それを使う文は閉じ波かっこの後に
;を付けて終わります。 - すべてのアームは共通の型の値を生成しなければなりません。
var x = n switch { 0 => "zero", _ => 0 };はコンパイルできません(CS8506、最適な型がない)。C# 9以降は、宣言された対象の型で決まります:object x = n switch { 0 => "zero", _ => 0 };はコンパイルできます。 - すべてのアームが値を生成しなければならないので、アームで
Console.WriteLineのようなvoidメソッドを呼ぶことはできず、switch式を単独の文として書くこともできません(CS0201)。動作を実行するならswitch文を使います。 - アームは値を生成する代わりに例外を投げられます:
_ => throw new ArgumentOutOfRangeException(nameof(input))。 - 前のアームがすでに網羅しているために到達できないアームは、エラーCS8510
The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match.でコンパイラに拒否されます。限定的なアームを一般的なアームより前に置きます。
関係パターンと論理パターン(C# 9)
C# 9で <、<=、>、>= がパターンとして、また and、or、not の組み合わせ子が追加されました。範囲の表が仕様書のように読めるようになります。
// C# 9 and later
string Grade(int score) => score switch
{
< 0 or > 100 => throw new ArgumentOutOfRangeException(nameof(score)),
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};
string AgeGroup(int age) => age switch
{
< 13 => "child",
>= 13 and <= 19 => "teen",
_ => "adult"
};
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
アームは順に試されるので、>= 80 に届くのは90未満の点数だけです。関係パターンの右辺には定数しか書けません。変数と比較するには when ガードを使います。範囲の表をC# 7で書くと if の連鎖になります。
出力:
95 -> A
83 -> B
71 -> C
64 -> D
12 -> F
not は not null の形で使われることが最も多く、x is not null や、not null => x.Name のようなアームに書きます。
プロパティパターン
プロパティパターンはオブジェクトのメンバーで照合します:{ Property: pattern, ... }。switch式と組み合わせると、&& の条件の連鎖なしでビジネスルールを表現できます。
// C# 8 and later
decimal ShippingCost(Order order) => order switch
{
{ Total: >= 100m } => 0m, // relational inside: C# 9
{ Country: "US", Express: true } => 15m,
{ Country: "US" } => 5m,
{ Express: true } => 30m,
null => throw new ArgumentNullException(nameof(order)),
_ => 12m
};
アームが一致するには、列挙したすべてのプロパティが一致しなければなりません。空のプロパティパターン { } はnullでない任意の値に一致します。C# 10では入れ子のメンバー向けに拡張プロパティパターンが追加され、{ Customer: { Address: { Country: "US" } } } の代わりに { Customer.Address.Country: "US" } と書けます。
タプルパターン
タプルでswitchすると複数の値を一度に照合でき、決定表をすっきり表現できます。
// C# 9 and later (the 'or' pattern)
string Winner(string a, string b) => (a, b) switch
{
("rock", "scissors") or ("scissors", "paper") or ("paper", "rock") => "player 1",
var (x, y) when x == y => "draw",
_ => "player 2"
};
var (x, y) はタプルを2つの変数に分解し、when ガードでそれらを比較できます。C# 7での一般的な代替は、値を1つのキーにまとめるか、結果の辞書を使う方法です。
出力:
player 1
player 2
draw
型パターンとwhen
アームは実行時の型を判定して変数を束縛でき、when 句でパターンの構文では表せない任意の条件を加えられます。
// C# 8 and later
double Area(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r when r.Width == r.Height => r.Width * r.Width,
Rectangle r => r.Width * r.Height,
null => throw new ArgumentNullException(nameof(shape)),
_ => throw new NotSupportedException(shape.GetType().Name)
};
C# 7版は if の連鎖で is 型パターンを使い、同じように変数を束縛します。
出力:
Circle: 12.57
Rectangle: 13.50
網羅性とSwitchExpressionException
switch式はすべての入力に対して値を生成しなければなりません。どのアームにも一致しない入力があるとコンパイラが判断できると、警告を出します。
warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '0' is not covered.
これはエラーではなく警告なので、コードはビルドできます。実行時に一致しない値が来ると、式は System.Runtime.CompilerServices.SwitchExpressionException を投げます。そのメッセージの末尾には一致しなかった値が含まれます(Unmatched value was 3.)。
enumは特別扱いです。名前の付いたすべてのメンバーにアームがあっても、enum変数は任意の整数を保持できる((Size)3)ので、コンパイラは「名前のないenum値」についてCS8524を報告します。
// C# 8 and later
enum Size { Small, Medium, Large }
string Code(Size s) => s switch
{
Size.Small => "S",
Size.Medium => "M",
Size.Large => "L",
// warning CS8524 without the next arm
_ => throw new ArgumentOutOfRangeException(nameof(s))
};
役に立つ例外を投げる明示的な _ アームを追加します。前提が文書化され、警告が消え、不正な値が来たときに SwitchExpressionException よりわかりやすいエラーになります。
代わりにDictionaryを使う
アームが純粋に定数と値の組で、表が大きくなったり設定から読み込まれたりする可能性があるなら、Dictionary がどのバージョンのC#でも同じ役割を果たし、実行時に組み立てることもできます。
出力:
19.00
11.50
0
未知の国の税率は 0m になります。decimal の積の小数点以下の桁数は2つのオペランドの桁数の合計になるので、100m * 0.19m は 19.00 と表示され、税率ゼロの場合は 0 と表示されます。
switch式とswitch文の比較
| switch文 | switch式 | |
|---|---|---|
| 使えるバージョン | C# 1 | C# 8 |
| 値を生成する | しない | する |
| 分岐ごとの構文 | case X: ... break; | X => value, |
| 受け皿 | default: | _ => |
| 分岐ごとに複数の文 | 可 | 不可、メソッドを呼ぶ |
| 網羅性の警告 | なし | あり(CS8509) |
| 実行時に一致しない場合 | 何も実行されない | SwitchExpressionException |
どちらの形も同じパターン(型、定数、プロパティ、タプル、関係)を受け付けるので、一方を学べばもう一方も身に付きます。選び方は形で決まります。値なら式、一連の動作なら文を使います。
よくある質問
C#のswitch式とは何ですか?
C# 8で追加されたswitch式は、評価されると値になります:string text = code switch { 200 => "OK", 404 => "Not Found", _ => "Unknown" };。入力は switch キーワードの前に置き、各アームは pattern => result の形で、アームはカンマで区切ります。case、break、default のキーワードはありません。
C#のswitch式でdefaultに当たるものは何ですか?
破棄パターン _ は何にでも一致するので、最後のアームに _ => "Unknown" を置くと default の役割を果たします。すべてを受け止めるアームがないと、網羅されていない入力があるときにコンパイラが警告(CS8509)を出し、実行時に一致しない値が来ると SwitchExpressionException が投げられます。
switch式の1つのアームで複数の値に一致させるには?
C# 9の or パターンを使います:"sat" or "sun" => "weekend"。範囲には関係パターンを and で組み合わせます:>= 13 and <= 19 => "teen"。C# 8では、同じ結果を持つ別々のアームとして値を並べます。
C#のswitch式でwhenは使えますか?
使えます。アームのパターンの後にガードを付けられます:Order o when o.Total > 100 => 0m。そのアームが一致するのは、パターンが一致し、かつ when の条件が真のときだけです。そうでなければ次のアームの評価に進みます。
switch式とswitch文のどちらを使うべきですか?
ステータスをラベルに対応付ける、価格を計算するなど、すべての分岐が値を生成するだけなら式を使います。分岐が複数の文を実行する、副作用がある、ループから抜ける必要がある場合は文を使います。式には、文にはない網羅性の警告もあります。