C#のモダン構文 — record・パターンマッチング・タプル
この章の目次開く
ここまでで、C#の基礎はひととおり揃いました。最後に、近年のC#を特徴づける3つの機能を扱います。いずれも「短く書ける」だけでなく、意図が伝わるコードを書くための道具です。
record — 値として扱うクラス
class は参照型なので、中身が同じでも別インスタンスなら等しくないと判定されます。
public class PointClass
{
public int X { get; init; }
public int Y { get; init; }
}
var a = new PointClass { X = 1, Y = 2 };
var b = new PointClass { X = 1, Y = 2 };
Console.WriteLine(a == b); // Falserecord として定義すると、中身が同じなら等しいという比較になります。
public record Point(int X, int Y);
var a = new Point(1, 2);
var b = new Point(1, 2);
Console.WriteLine(a == b); // True
Console.WriteLine(a); // Point { X = 1, Y = 2 }record を使うと、次のものが自動的に用意されます。
| 自動生成されるもの | 効果 |
|---|---|
等価比較(== / Equals) | 中身が同じなら等しい |
GetHashCode | 等価比較と整合する |
ToString | 中身が読める形で出力される |
with 式 | 一部だけ変えたコピーを作れる |
| 分解(デコンストラクト) | 各プロパティを変数に取り出せる |
var moved = a with { X = 10 }; // Point { X = 10, Y = 2 }
var (x, y) = a; // x = 1, y = 2record は「値そのものを表すデータ」に使い、class は「振る舞いや状態を持つオブジェクト」に使う、という切り分けが基本です。
| 使うもの | 向いている対象 |
|---|---|
record | APIのレスポンス、設定値、座標、金額、検索条件 |
class | サービス、リポジトリ、状態を持ち回るオブジェクト |

タプル — 名前を付けずに複数の値を返す
複数の値を返したいだけなら、クラスを作らずタプルで済ませられます。
public (int Min, int Max) GetRange(List<int> values)
{
return (values.Min(), values.Max());
}
var range = GetRange(scores);
Console.WriteLine(range.Min);
var (min, max) = GetRange(scores); // 分解して受け取る要素に名前を付けられるため、Item1 / Item2 のような読みにくいコードになりません。
パターンマッチング
第4章 で switch 式に触れましたが、C#の分岐は「値」だけでなく「形」でも判定できます。
型パターン
object value = 42;
string description = value switch
{
int n when n > 100 => "大きい整数",
int n => $"整数: {n}",
string s => $"文字列: {s}",
null => "null",
_ => "その他"
};int n のように、判定と同時に変数へ取り出せます。第7章 の is と同じ仕組みです。
プロパティパターン
オブジェクトの中身の値で分岐できます。
string GetShipping(Order order) => order switch
{
{ Total: > 10000 } => "送料無料",
{ Address.Prefecture: "沖縄県" } => "離島料金",
{ IsExpress: true } => "速達",
_ => "通常配送"
};{ Address.Prefecture: "沖縄県" } のように、入れ子のプロパティも直接書けます。
リレーショナルパターンと論理パターン
string Judge(int score) => score switch
{
< 0 or > 100 => "不正な値",
>= 90 => "S",
>= 70 and < 90 => "A",
_ => "B"
};and / or / not を組み合わせられるため、複雑な条件も1つの式で表現できます。
学習者if文をいくつも書くのと、何が違うんでしょう?
分岐が式になるため、結果を必ず1つの値として受け取れる点が違います。「どの分岐でも変数への代入を忘れない」ことがコンパイラによって保証され、条件の並びも縦に揃うので比較しやすくなります。
recordとの組み合わせ
record は分解に対応しているため、位置パターンも使えます。
public record Point(int X, int Y);
string Where(Point p) => p switch
{
(0, 0) => "原点",
(var x, 0) => $"X軸上({x})",
(0, var y) => $"Y軸上({y})",
_ => "その他"
};record とパターンマッチングは組み合わせて使うことで力を発揮します。データを record で表し、switch 式で形に応じて処理を決める、という書き方がモダンなC#の定番です。
nullの判定
if (user is null) { }
if (user is not null) { }== null より意図が明確で、演算子が上書きされている型でも確実に動きます。第10章 で扱ったnullチェックでも、この書き方が推奨されます。
よくあるハマりどころ
recordの中に可変なコレクションを入れる
public record Basket(List<string> Items);record 自体は等価比較を提供しますが、List は参照型なので中身が同じでも別インスタンスなら等しくなりません。加えて、外から中身を変更できてしまいます。不変にしたいなら IReadOnlyList<string> を使い、生成時に確定させます。
switch式で _ を書かない
どのパターンにも一致しなかった場合、実行時に例外が発生します。enum を網羅したつもりでも、将来値が追加されると漏れます。_ は必ず用意してください。
パターンを詰め込みすぎる
1つの switch 式に10個以上の分岐や複雑な条件を並べると、かえって読みにくくなります。判定の意味ごとにメソッドへ切り出すほうが読み手に親切です。
先生新しい構文は「短く書ける」より「意図が伝わる」ときに使う。これは他の言語でも同じだね。
ちゃんと使うためのポイント
- データを表す型は
record、振る舞いを持つ型はclass with式で「一部だけ違うコピー」を安全に作れる- 内部で複数の値を返すならタプル、公開APIなら
record switch式は値・型・プロパティ・範囲で分岐でき、結果を1つの値として返す- nullの判定は
is null/is not null _を必ず用意して、想定外の値で落ちないようにする
この本を読み終えたら
ここまでで、C#の言語としての基礎はひととおり押さえました。次に進む先は目的によって変わります。
- Web APIを作る — ASP.NET Coreへ。設計の考え方は Web APIとHTTPの基礎 が土台になります
- データベースを扱う — Entity Framework Coreへ。その前に SQLとデータベースの基礎 でSQLを押さえておくと理解が早くなります
- テストを書く — xUnitへ。考え方は ユニットテスト入門 がそのまま応用できます
言語の文法は入口にすぎません。実際に小さなアプリを1つ作り切ると、この本で扱った内容が一気につながります。