- C#
C# switch文とswitch式の書き方|パターンマッチングとwhen
C#のswitchには、処理を分ける「switch文」と、値を返す「switch式」の2種類があります。 どちらも同じパターンマッチング(値の形や範囲で分岐する仕組み)を使えるので、片方を覚えればもう片方もすぐに書けます。違いは「複数行の処理を書きたいか」「1つの値を返したいか」です。
この記事では、同じ処理をswitch文とswitch式の両方で書き比べながら、型・定数・関係・論理・プロパティ・タプル・リストの各パターンを順に紹介します。コードはすべて .NET SDK 8.0.131(C# 12)で実行し、出力とコンパイラのメッセージをそのまま載せました。C# 13・C# 14(.NET 10)については、Microsoft Learn の「C# 言語バージョン履歴」と「C# 14 の新機能」を今回開いて確認し、パターンマッチングとswitchに追加がないことを確かめています(C# 13・14 での実行は未実行です)。
switch文とswitch式の早見表
まず、2つの書き方の違いです。
| 比べる点 | switch文 | switch式 |
|---|---|---|
| 使えるバージョン | C# 1.0 から(パターンは C# 7.0 から) | C# 8.0 から |
| 形 | switch (値) { case パターン: 処理; break; } |
値 switch { パターン => 結果, } |
| 結果 | 値を返さない(中で return や代入をする) |
式全体が1つの値になる |
| 1つの分岐に書けるもの | 複数の文 | 式を1つだけ |
| 「それ以外」の書き方 | default: |
_ =>(破棄パターン) |
| 分岐の終わり | break・return・throw などが必要 |
不要(カンマで区切る) |
| 複数条件をまとめる | case を続けて書く、または or |
or |
| 追加の条件 | case パターン when 条件: |
パターン when 条件 => |
| 網羅していないとき | 何も起きずに通り抜ける | 警告 CS8509、実行時は例外 |
| 向いている場面 | 分岐ごとに複数行の処理をする | 値を別の値に対応づける |
次に、switch文・switch式・is式で共通して使えるパターンの一覧です。「使えるC#バージョン」は Microsoft Learn の言語バージョン履歴で確認しました。
| パターン | 書き方の例 | 意味 | 使えるC#バージョン |
|---|---|---|---|
| 定数 | 1、"yes"、null、DayOfWeek.Sunday |
その値と等しい | 1.0(switch文のcase)/7.0(is式) |
| 宣言 | int n、string s |
その型なら変数に入れる | 7.0 |
| var | var x |
何にでも一致し変数に入れる | 7.0 |
| whenガード | int n when n < 0 |
パターンに条件式を足す | 7.0 |
| 破棄 | _ |
何にでも一致(それ以外) | 8.0 |
| プロパティ | { Total: >= 5000 } |
プロパティの値で判定 | 8.0 |
| タプル | (true, "admin") |
複数の値を同時に判定 | 8.0 |
| 位置指定 | (0, 0)(Deconstructを持つ型) |
分解した値で判定 | 8.0 |
| 型 | int、string(変数なし) |
その型かどうかだけ見る | 9.0 |
| 関係 | < 0、>= 90 |
定数との大小比較 | 9.0 |
| 論理 | >= 0 and < 15、1 or 2、not null |
パターンの組み合わせ | 9.0 |
| かっこ | not (1 or 2) |
組み合わせの優先順位を示す | 9.0 |
| 拡張プロパティ | { Member.Rank: "Gold" } |
入れ子のプロパティをドットで書く | 10 |
| リスト | [1, 2, 3]、[var first, ..] |
配列やリストの並びで判定 | 11 |
| スライス | [.., var last]、[_, .. var rest] |
リストの「残り」を表す | 11 |
C# 12・13・14 では、パターンマッチングとswitchに関する追加はありません(言語バージョン履歴で確認)。そのため、C# 11 までの内容を押さえれば、C# 14 / .NET 10 でもそのまま使えます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「C# switch文とswitch式の書き方|パターンマッチングとwhen」
確認した環境とコードの動かし方
この記事のコードは、次の手順でそのまま動かせます。
dotnet --version
dotnet new console -o SwitchDemo
cd SwitchDemo
# Program.cs を記事のコードに置き換えてから
dotnet run
| 項目 | 内容 |
|---|---|
| .NET SDK | 8.0.131 |
| ランタイム | .NET 8.0.31 |
| C# のバージョン | 12(.NET 8 の既定) |
| 書き方 | トップレベルステートメント(Mainメソッドを省略する書き方) |
コードは Program.cs にそのまま貼れる形にしています。record や enum の定義は、ファイルの末尾に置いてください(トップレベルステートメントでは、型の定義を文より後ろに書く決まりがあります)。
switch文とswitch式を同じ処理で書き比べる
点数からランクを求める処理を、両方の書き方で書きます。
// switch 文:case ごとに return する
static string RankStmt(int score)
{
switch (score)
{
case >= 90:
return "A";
case >= 70:
return "B";
case >= 50:
return "C";
default:
return "D";
}
}
// switch 式:式全体が値になる
static string RankExpr(int score) => score switch
{
>= 90 => "A",
>= 70 => "B",
>= 50 => "C",
_ => "D",
};
foreach (var s in new[] { 95, 70, 55, 10 })
Console.WriteLine($"{s}: 文={RankStmt(s)} 式={RankExpr(s)}");
実行結果:
95: 文=A 式=A
70: 文=B 式=B
55: 文=C 式=C
10: 文=D 式=D
結果は同じです。書き方の対応は次のとおりです。
| switch文 | switch式 |
|---|---|
switch (score) { ... } |
score switch { ... } |
case >= 90: |
>= 90 => |
return "A"; |
"A", |
default: |
_ => |
switch式は「値 switch」の順で書き、各行(アームと呼びます)をカンマで区切ります。 最後のアームの後ろのカンマは付けても付けなくてもかまいません。付けておくと、行を足したときの差分が1行で済みます。
上から順に判定される
どちらのswitchも、上から順にパターンを確かめ、最初に一致したところで決まります。 上の例で 95 点は >= 70 にも >= 50 にも一致しますが、先に書いた >= 90 が選ばれます。範囲で分けるときは、狭い条件を上に書くのが基本です。順番を入れ替えるとコンパイラが教えてくれます(後述の CS8510)。
どちらを選ぶかの判断表
| やりたいこと | 選ぶもの | 理由 |
|---|---|---|
| 値を別の値に対応づける(コード→名称、点数→ランク) | switch式 | 短く書け、網羅漏れを警告してくれる |
| メソッドの戻り値をそのまま決める | switch式 | => 値 switch { ... }; と1つの式で書ける |
| 分岐ごとに複数行の処理(ログ出力+更新など) | switch文 | 1つのcaseに複数の文を書ける |
分岐の中でループを抜けたり continue したい |
switch文 | 式の中には文を書けない |
| 戻り値のないメソッドを呼び分けるだけ | switch文 | switch式は値が必要 |
| ある case の後に別の case の処理も続けたい | switch文 | goto case が使える |
迷ったら、「結果が1つの値になるならswitch式、そうでなければswitch文」 と考えると選びやすくなります。
パターン別の書き方(switch文とswitch式の両方)
ここからは、パターンごとに両方の書き方を並べます。
型で分ける(型パターン・宣言パターン)
object 型や基底クラスの変数を、実際の型で分けます。int n のように書くと、一致したときに変換済みの変数 n が使えます。
// 型パターン:switch 文と switch 式
static string DescribeStmt(object? o)
{
switch (o)
{
case null:
return "null です";
case int n:
return $"整数 {n}";
case string s:
return $"文字列 {s}({s.Length}文字)";
default:
return $"その他 {o.GetType().Name}";
}
}
static string DescribeExpr(object? o) => o switch
{
null => "null です",
int n => $"整数 {n}",
string s => $"文字列 {s}({s.Length}文字)",
_ => $"その他 {o.GetType().Name}",
};
foreach (var o in new object?[] { null, 42, "RJC", 3.14 })
Console.WriteLine($"{DescribeStmt(o)} / {DescribeExpr(o)}");
実行結果:
null です / null です
整数 42 / 整数 42
文字列 RJC(3文字) / 文字列 RJC(3文字)
その他 Double / その他 Double
押さえておきたい点は3つです。
int nやstring sは null に一致しません。 null を扱いたいときはnullのアームを別に書きます。- 変数が要らないときは
int =>のように型名だけで書けます(C# 9.0 以降)。 var xは null を含めて何にでも一致します。_と違って値を変数に受け取れるので、when と組み合わせるときに便利です。
範囲で分ける(関係パターンと論理パターン)
<、<=、>、>= を使った関係パターンと、and・or・not の論理パターンは C# 9.0 から使えます。
// 関係パターンと論理パターン
static string TempStmt(int c)
{
switch (c)
{
case < 0:
return "氷点下";
case >= 0 and < 15:
return "寒い";
case >= 15 and < 25:
return "快適";
default:
return "暑い";
}
}
static string TempExpr(int c) => c switch
{
< 0 => "氷点下",
>= 0 and < 15 => "寒い",
>= 15 and < 25 => "快適",
_ => "暑い",
};
foreach (var c in new[] { -2, 0, 14, 15, 30 })
Console.WriteLine($"{c}: {TempStmt(c)} / {TempExpr(c)}");
// is 式でも同じパターンが使える
static bool IsLetter(char ch) => ch is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Console.WriteLine($"{IsLetter('q')} {IsLetter('5')}");
// not の範囲に注意
int x = 2;
Console.WriteLine(x is not 1 or 2); // (not 1) or 2
Console.WriteLine(x is not (1 or 2)); // 1 でも 2 でもない
実行結果:
-2: 氷点下 / 氷点下
0: 寒い / 寒い
14: 寒い / 寒い
15: 快適 / 快適
30: 暑い / 暑い
True False
True
False
論理パターンの優先順位は、not → and → or の順です。最後の2行がその例で、not 1 or 2 は「1 ではない、または 2」と読まれるため、2 を渡すと True になります。「1 でも 2 でもない」と書きたいときは not (1 or 2) とかっこで囲みます。
| 書き方 | 読まれ方 | x が 2 のとき |
|---|---|---|
x is not 1 or 2 |
(1ではない) または 2 | True |
x is not (1 or 2) |
1 でも 2 でもない | False |
x is not 1 and not 2 |
1 ではなく、かつ 2 でもない | False |
なお、関係パターンの右側に書けるのは定数だけです。変数と比べたいときは when を使います(後述)。
プロパティの値で分ける(プロパティパターン)
オブジェクトのプロパティを { プロパティ名: パターン } の形で調べます。入れ子のプロパティは、C# 10 から Member.Rank のようにドットでつなげて書けます。
// プロパティパターン
static string ShippingStmt(Order o)
{
switch (o)
{
case { Total: >= 5000 }:
return "送料無料";
case { Member.Rank: "Gold" }:
return "送料無料(ゴールド会員)";
case { Member.Rank: "Silver", Total: >= 3000 }:
return "送料半額";
default:
return "通常送料";
}
}
static string ShippingExpr(Order o) => o switch
{
{ Total: >= 5000 } => "送料無料",
{ Member.Rank: "Gold" } => "送料無料(ゴールド会員)",
{ Member.Rank: "Silver", Total: >= 3000 } => "送料半額",
_ => "通常送料",
};
var orders = new[]
{
new Order(6000, new Member("None")),
new Order(1000, new Member("Gold")),
new Order(3500, new Member("Silver")),
new Order(1000, new Member("Silver")),
};
foreach (var o in orders)
Console.WriteLine($"{o.Total}円 {o.Member.Rank}: {ShippingStmt(o)} / {ShippingExpr(o)}");
record Member(string Rank);
record Order(int Total, Member Member);
実行結果:
6000円 None: 送料無料 / 送料無料
1000円 Gold: 送料無料(ゴールド会員) / 送料無料(ゴールド会員)
3500円 Silver: 送料半額 / 送料半額
1000円 Silver: 通常送料 / 通常送料
プロパティパターンの書き方をまとめます。
| 書き方 | 意味 | バージョン |
|---|---|---|
{ Total: >= 5000 } |
Total が 5000 以上 | 8.0(関係パターンは 9.0) |
{ Member: { Rank: "Gold" } } |
入れ子のプロパティ(波かっこを重ねる) | 8.0 |
{ Member.Rank: "Gold" } |
同じ意味をドットで短く書く | 10 |
{ Rank: "Silver", Total: >= 3000 } |
カンマは「かつ」 | 8.0 |
{ } |
null ではない(中身は問わない) | 8.0 |
{ Length: 0 } |
文字列や配列が空 | 8.0 |
{ Total: var t } |
プロパティの値を変数に取り出す | 8.0 |
プロパティパターンは、対象が null のときは一致しません。 そのため { } を「null ではない」という判定として使えます。
複数の値の組み合わせで分ける(タプルパターン・位置指定パターン)
2つ以上の値を同時に見たいときは、(a, b) switch のようにタプルにして判定します。if を入れ子にする代わりに、組み合わせを表のように並べて書けます。
// タプルパターンと位置指定パターン
static string JankenStmt(string a, string b)
{
switch (a, b)
{
case ("グー", "チョキ"):
case ("チョキ", "パー"):
case ("パー", "グー"):
return "aの勝ち";
case var (x, y) when x == y:
return "あいこ";
default:
return "bの勝ち";
}
}
static string JankenExpr(string a, string b) => (a, b) switch
{
("グー", "チョキ") or ("チョキ", "パー") or ("パー", "グー") => "aの勝ち",
var (x, y) when x == y => "あいこ",
_ => "bの勝ち",
};
Console.WriteLine($"{JankenStmt("グー", "チョキ")} / {JankenExpr("グー", "チョキ")}");
Console.WriteLine($"{JankenStmt("パー", "パー")} / {JankenExpr("パー", "パー")}");
Console.WriteLine($"{JankenStmt("グー", "パー")} / {JankenExpr("グー", "パー")}");
// 位置指定パターン:record の分解(Deconstruct)を使う
static string Quadrant(Point p) => p switch
{
(0, 0) => "原点",
(> 0, > 0) => "第1象限",
(< 0, > 0) => "第2象限",
(_, 0) or (0, _) => "軸の上",
_ => "第3・第4象限",
};
Console.WriteLine($"{Quadrant(new(0, 0))} {Quadrant(new(2, 3))} {Quadrant(new(-1, 5))} {Quadrant(new(4, 0))} {Quadrant(new(1, -1))}");
record Point(int X, int Y);
実行結果:
aの勝ち / aの勝ち
あいこ / あいこ
bの勝ち / bの勝ち
原点 第1象限 第2象限 軸の上 第3・第4象限
- タプルパターンは、その場で作ったタプル
(a, b)を各位置のパターンで判定します。_を置くと「その位置は何でもよい」という意味になります。 - 位置指定パターンは、Deconstruct メソッドを持つ型(record はこれを自動で持ちます)を分解して判定します。書き方はタプルと同じです。
var (x, y)と書くと、各位置の値を変数に受け取れます。when と組み合わせると、位置どうしの比較ができます。
配列やリストの並びで分ける(リストパターン)
C# 11 から、配列や List の要素の並びをパターンで書けるようになりました。.. は「0個以上の残り」を表すスライスパターンで、1つのリストパターンの中で1回だけ使えます。
// リストパターン(C# 11 以降)
static string RunStmt(string[] args)
{
switch (args)
{
case []:
return "引数なし";
case ["help"]:
return "ヘルプ表示";
case ["add", var name]:
return $"追加: {name}";
case ["remove", .. var rest]:
return $"削除: {rest.Length}件";
case [var first, .., var last]:
return $"先頭={first} 末尾={last}";
default:
return "不明なコマンド";
}
}
static string RunExpr(string[] args) => args switch
{
[] => "引数なし",
["help"] => "ヘルプ表示",
["add", var name] => $"追加: {name}",
["remove", .. var rest] => $"削除: {rest.Length}件",
[var first, .., var last] => $"先頭={first} 末尾={last}",
_ => "不明なコマンド",
};
string[][] inputs =
{
new string[0],
new[] { "help" },
new[] { "add", "pen" },
new[] { "remove", "a", "b", "c" },
new[] { "x", "y", "z" },
new[] { "zzz" },
};
foreach (var a in inputs)
Console.WriteLine($"[{string.Join(" ", a)}] {RunStmt(a)} / {RunExpr(a)}");
// List<T> や文字列にも使える
var list = new List<int> { 1, 2, 3, 4 };
Console.WriteLine(list is [1, _, ..]);
Console.WriteLine("hello" is ['h', .., 'o']);
実行結果:
[] 引数なし / 引数なし
[help] ヘルプ表示 / ヘルプ表示
[add pen] 追加: pen / 追加: pen
[remove a b c] 削除: 3件 / 削除: 3件
[x y z] 先頭=x 末尾=z / 先頭=x 末尾=z
[zzz] 不明なコマンド / 不明なコマンド
True
True
| 書き方 | 一致するもの |
|---|---|
[] |
要素が0個 |
[_] |
要素がちょうど1個 |
[1, 2, 3] |
1, 2, 3 の3要素 |
[var first, ..] |
1個以上。先頭を first に受け取る |
[.., var last] |
1個以上。末尾を last に受け取る |
[var first, .., var last] |
2個以上。先頭と末尾を受け取る |
["remove", .. var rest] |
先頭が "remove"。残りを rest に受け取る |
[> 0, ..] |
先頭が正の数(他のパターンを中に書ける) |
[var first, .., var last] は2要素以上が必要なので、1要素の ["zzz"] は一致せず、最後のアームに進んでいます。
リストパターンを C# 10 の設定でコンパイルすると、次のエラーになります(LangVersion を 10 に指定して確認)。
error CS8936: Feature 'list pattern' is not available in C# 10.0. Please use language version 11.0 or greater.
このエラーが出たときは、.NET 7 以降の SDK でビルドするか、プロジェクトファイルの LangVersion を確認します。
複数条件の書き方を用途別に選ぶ
「複数条件」と言っても、やりたいことは大きく4つに分かれます。それぞれ向いた書き方があります。
| やりたいこと | 書き方 | 使える場所 |
|---|---|---|
| A または B のとき同じ処理 | case A: と case B: を続けて書く |
switch文 |
| A または B のとき同じ結果 | A or B |
switch文・switch式(C# 9.0〜) |
| 範囲(A 以上 B 未満) | >= A and < B |
switch文・switch式(C# 9.0〜) |
| パターンに加えて別の変数も見る | パターン when 条件 |
switch文・switch式 |
| 2つ以上の値の組み合わせ | タプル (x, y) switch |
switch文・switch式(C# 8.0〜) |
まとめて動かせるコードです。
// 複数条件の書き方
// (1) case を続けて書く(switch 文)
static string KindA(DayOfWeek d)
{
switch (d)
{
case DayOfWeek.Saturday:
case DayOfWeek.Sunday:
return "休日";
default:
return "平日";
}
}
// (2) or でまとめる(switch 文でも switch 式でも使える)
static string KindB(DayOfWeek d) => d switch
{
DayOfWeek.Saturday or DayOfWeek.Sunday => "休日",
_ => "平日",
};
// (3) when で変数どうしの条件を足す
static string Fee(int age, bool member) => age switch
{
< 6 => "無料",
< 13 when member => "300円",
< 13 => "500円",
>= 65 when member => "600円",
_ => "1000円",
};
// (4) 複数の値を同時に見るならタプル
static string Access(bool loggedIn, string role) => (loggedIn, role) switch
{
(false, _) => "ログインが必要",
(true, "admin") => "すべて操作できる",
(true, "editor" or "author") => "記事を編集できる",
(true, _) => "閲覧のみ",
};
Console.WriteLine($"{KindA(DayOfWeek.Sunday)} {KindB(DayOfWeek.Sunday)} {KindB(DayOfWeek.Monday)}");
Console.WriteLine($"{Fee(3, false)} {Fee(10, true)} {Fee(10, false)} {Fee(70, true)} {Fee(30, true)}");
Console.WriteLine($"{Access(false, "admin")} / {Access(true, "admin")} / {Access(true, "author")} / {Access(true, "guest")}");
実行結果:
休日 休日 平日
無料 300円 500円 600円 1000円
ログインが必要 / すべて操作できる / 記事を編集できる / 閲覧のみ
caseを続けて書く
switch文では、中身が空の case は続けて書けます。 上の KindA では、土曜と日曜が同じ処理に進みます。C や Java のように「処理を書いた case から次の case へそのまま流れる」ことはできません(CS0163 の節で説明します)。
orでまとめる
C# 9.0 以降は A or B と1行で書けます。switch式には「case を続ける」書き方がないので、switch式で複数の値をまとめるときは or を使います。 上の Access のように、タプルの中の1か所だけに or を書くこともできます。
なお、パターンの中で使えるのは or であり、|| は使えません。|| と && は when の後ろの条件式で使います。
whenで条件を足す
when は、パターンが一致したあとに追加で確かめる条件式です。パターンに書けるのは定数だけなので、変数やメソッドの結果と比べたいときに when を使います。
| 書きたい条件 | パターンで書ける? | 書き方 |
|---|---|---|
| 定数との比較(13 未満) | 書ける | < 13 |
| 別の変数が true のとき | 書けない | < 13 when member |
| 変数との比較(limit 以上) | 書けない | var n when n >= limit |
| メソッドの結果 | 書けない | string s when s.StartsWith("A") |
| 位置どうしの比較 | 書けない | var (x, y) when x == y |
Fee の例では、同じ < 13 を2回書いています。上のアームは「13未満で、かつ会員」、下は「13未満(会員でない場合がここに来る)」です。when が付いたアームは、条件が false なら次のアームに進むので、このように段階的に書けます。
変数と比べる例を1つ示します。パターンに変数を書くと、次のエラーになります。
int limit = 10;
int n = 5;
string r = n switch
{
limit => "上限",
_ => "それ以外",
};
error CS9135: A constant value of type 'int' is expected
直し方は2通りあります。
// 直し方1:limit を const にする
const int Limit = 10;
string r1 = n switch { Limit => "上限", _ => "それ以外" };
// 直し方2:when で比べる
string r2 = n switch { var v when v == limit => "上限", _ => "それ以外" };
つまずきやすいエラーと直し方
ここからは、switchを書くときによく出会うコンパイラのメッセージを、再現コードと直し方のセットで紹介します。メッセージは .NET SDK 8.0.131 の出力です。
| コード | 種類 | 出るとき | 直し方 |
|---|---|---|---|
| CS8509 | 警告 | switch式がすべての値を扱っていない | _ => を足す、または不足のアームを足す |
| CS8524 | 警告 | enum の名前付きの値は揃っているが、名前のない値が残る | _ => throw ... を足す |
| CS8510 | エラー | switch式で、上のアームが先に一致してしまう | 狭い条件を上に並べ替える |
| CS8120 | エラー | switch文で、上の case が先に一致してしまう | 狭い条件を上に並べ替える |
| CS0163 | エラー | case の処理が次の case へ流れ込む | break・return・goto case を書く |
| CS8070 | エラー | 最後の case(default)に break がない |
break を書く |
| CS9135 | エラー | パターンに変数を書いた | const にするか when を使う |
| CS0201 | エラー | switch式を文として単独で書いた | 結果を変数で受ける、またはswitch文にする |
CS8509:switch式が網羅されていない
static string Rank(int score) => score switch
{
>= 90 => "A",
>= 70 => "B",
};
Console.WriteLine(Rank(95));
Console.WriteLine(Rank(10));
ビルド時の警告:
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.
警告なのでビルドは通りますが、扱っていない値(ここでは 10)を渡すと、実行時に例外になります。
A
Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.
Unmatched value was 10.
(スタックトレースの行は省略しています。)
コンパイラは「たとえば 0 が扱われていません」と、漏れている値の例まで教えてくれます。 直し方は、残りを受け止めるアームを足すことです。
static string Rank(int score) => score switch
{
>= 90 => "A",
>= 70 => "B",
_ => "C",
};
同じ警告は、object に対して型のアームだけを並べたときにも出ます。次のコードでは「not null が扱われていません」と表示されました。
object o = 5;
string r = o switch
{
int n => "int",
string s => "str",
null => "null",
};
warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'not null' is not covered.
CS8524:enumの値をすべて書いたのに警告が出る
enum の値を全部書いても、次の警告が出ます。
static string Label(Status s) => s switch
{
Status.Todo => "未着手",
Status.Doing => "進行中",
Status.Done => "完了",
};
enum Status { Todo, Doing, Done }
warning CS8524: The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(Status)3' is not covered.
enum の実体は整数なので、(Status)99 のように名前のない値をキャストで作れるためです。実際に渡すと、CS8509 のときと同じ例外になりました。
System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.
Unmatched value was 99.
対処は、最後に _ のアームを置き、わかりやすい例外を投げることです。
// enum を switch 式で分ける
static string Label(Status s) => s switch
{
Status.Todo => "未着手",
Status.Doing => "進行中",
Status.Done => "完了",
_ => throw new ArgumentOutOfRangeException(nameof(s), s, "未対応の状態です"),
};
Console.WriteLine(Label(Status.Doing));
try
{
Console.WriteLine(Label((Status)99));
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine(ex.Message);
}
enum Status { Todo, Doing, Done }
実行結果:
進行中
未対応の状態です (Parameter 's')
Actual value was 99.
_ を置くかどうかは、次のように考えると決めやすくなります。
| 方針 | 書き方 | 良い点 | 気をつける点 |
|---|---|---|---|
_ => throw を置く |
上の例 | 警告が消え、想定外の値に気づきやすい例外になる | enum に値を足したとき、警告では気づけなくなる |
_ を置かず CS8524 だけ抑える |
プロジェクトファイルの NoWarn に CS8524 を指定する | enum に値を足すと CS8509 で漏れを教えてくれる(Hold を足して「Status.Hold is not covered」と出ることを確認) | 名前のない値は SwitchExpressionException になる |
CS8510・CS8120:到達しないアームがある
上のアームが広すぎて、下のアームに絶対に届かない場合は、警告ではなくエラーになります。
static string Rank(int score) => score switch
{
>= 70 => "B",
>= 90 => "A",
_ => "D",
};
error CS8510: The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match.
90以上の値はすべて、先に書いた >= 70 に一致してしまうためです。switch文で同じ並びにすると、番号だけ違う CS8120 になります。
error CS8120: The switch case is unreachable. It has already been handled by a previous case or it is impossible to match.
直し方は、狭い条件を上に、広い条件を下に並べ替えることです。
static string Rank(int score) => score switch
{
>= 90 => "A",
>= 70 => "B",
_ => "D",
};
型で分けるときも同じです。たとえば object o => を string s => より上に書くと、string のアームに届かなくなり、同じエラーになります。派生クラス(より具体的な型)を上に書きます。
CS0163・CS8070:caseの終わりにbreakがない
switch文では、case に処理を書いたら、その case を必ず終わらせる必要があります。
int n = 1;
switch (n)
{
case 1:
Console.WriteLine("one");
case 2:
Console.WriteLine("two");
break;
}
error CS0163: Control cannot fall through from one case label ('case 1:') to another
C# では、処理を書いた case から次の case へ自動で流れる動き(フォールスルー)を許していません。書き忘れによる意図しない動作を、コンパイルの時点で防ぐための決まりです。最後の case や default も同じで、break を省くと次のエラーになります。
error CS8070: Control cannot fall out of switch from final case label ('default:')
直し方は、目的に応じて選びます。
| 目的 | 書くもの |
|---|---|
| その case で終わる | break; |
| メソッドごと抜ける | return 値; |
| 例外にする | throw new ...; |
| 次の case の処理も続けたい | goto case 値; または goto default; |
| ループの次の周へ進む | continue;(ループの中のswitch文で) |
「次の case の処理も続けたい」ときは、goto case を使って明示します。
// goto case で次の case へ進む(switch 文だけの機能)
static int Price(string size)
{
int price = 0;
switch (size)
{
case "L":
price += 100;
goto case "M";
case "M":
price += 50;
goto default;
default:
price += 300;
break;
}
return price;
}
Console.WriteLine($"S={Price("S")}円 M={Price("M")}円 L={Price("L")}円");
実行結果:
S=300円 M=350円 L=450円
CS0201:switch式を文として書いた
switch式は値を返す「式」なので、結果を使わずに単独で書くとエラーになります。
int n = 1;
n switch
{
1 => Console.WriteLine("one"),
_ => Console.WriteLine("other"),
};
error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
直し方は、値を作ってから使う形にすることです。
int n = 1;
string text = n switch
{
1 => "one",
_ => "other",
};
Console.WriteLine(text);
戻り値のないメソッドを呼び分けたいだけなら、switch文を使います。
switchを読みやすく保つコツ
最後に、書くときのチェックリストです。
| チェック項目 | 理由 |
|---|---|
| 狭い条件を上、広い条件を下に並べた | 上から順に判定されるため |
switch式の最後に _ があるか、網羅できている |
CS8509 と実行時の例外を防ぐ |
_ で握りつぶさず、想定外は例外にした |
値が増えたときに気づける |
| null が来る可能性を考えた | 型パターンとプロパティパターンは null に一致しない |
| not と or を組み合わせるときはかっこを付けた | 優先順位は not → and → or |
| 変数との比較は when に書いた | パターンに書けるのは定数だけ |
| 1つのアームが長くなったらメソッドに切り出した | switch式のアームは式1つ |
| 文字列は大文字小文字をそろえてから判定した | 定数パターンは完全一致で比べる |
最後の項目の例です。文字列の定数パターンは大文字と小文字を区別するので、先にそろえてから判定します。
static string Answer(string s) => s.ToLowerInvariant() switch
{
"y" or "yes" => "はい",
"n" or "no" => "いいえ",
_ => "もう一度入力",
};
Console.WriteLine($"{Answer("YES")} {Answer("n")} {Answer("?")}");
実行結果:
はい いいえ もう一度入力
よくある質問
Q. switch文とswitch式で、速さに違いはありますか? A. 今回は速度の計測をしていません(未実行)。どちらも同じパターンマッチングの仕組みを使うため、選ぶ基準は速さよりも「値を返したいか、複数行の処理を書きたいか」に置くのがおすすめです。
Q. switch式の中で複数行の処理を書きたいときは? A. switch式のアームに書けるのは式1つです。処理が長い場合は、その部分をメソッドに切り出して呼び出すか、switch文に切り替えます。
Q. defaultや _ は必ず書く必要がありますか?
A. switch文では省略でき、どの case にも一致しなければ何もせずに次へ進みます。switch式では、すべての値を扱えていないと警告 CS8509 が出て、実行時に SwitchExpressionException になることがあるため、_ のアームを置くか、すべての場合を書き切ります。
Q. C# 14(.NET 10)でswitchに新しい書き方は増えましたか? A. Microsoft Learn の「C# 14 の新機能」と言語バージョン履歴を確認したところ、C# 12〜14 にパターンマッチングとswitchの追加はありませんでした。この記事の内容(C# 11 のリストパターンまで)がそのまま使えます。なお、同じ Microsoft Learn のパターンのページには、C# 15 として新しいパターンの記載がありました。こちらは今回の検証の対象外です。
まとめ
- switch文は処理を分け、switch式は値を返す。 値の対応づけはswitch式、複数行の処理はswitch文が向いています。
- パターンはどちらでも共通。 型・定数・関係・論理・プロパティ・タプル・リストの各パターンを、文でも式でも同じように書けます。
- 使えるバージョンは、switch式とプロパティ・タプルが C# 8.0、関係と論理が 9.0、リストが 11。 C# 12〜14 では追加がありません。
- 複数条件は用途で選ぶ。 同じ結果にまとめるなら or、範囲なら and、変数と比べるなら when、値の組み合わせならタプルです。
- 上から順に判定されるので、狭い条件を上に書きます。逆にすると CS8510(文では CS8120)で教えてくれます。
- CS8509 は網羅漏れの警告。
_のアームを足すか、足りない場合を書き足します。enum では CS8524 も確認します。 - switch文の case は break などで必ず終える。 次の case に続けたいときは goto case と書きます(CS0163)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「C# switch文とswitch式の書き方|パターンマッチングとwhen」
株式会社RJCでは、C#のswitchやパターンマッチングの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。