本文へスキップ
ウェブエンジニア問題集
第14章

C#のモダン構文 — record・パターンマッチング・タプル

約7分
この章の目次開く

ここまでで、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);  // False
csharp

record として定義すると、中身が同じなら等しいという比較になります。

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 }
csharp

record を使うと、次のものが自動的に用意されます。

自動生成されるもの効果
等価比較(== / Equals)中身が同じなら等しい
GetHashCode等価比較と整合する
ToString中身が読める形で出力される
with 式一部だけ変えたコピーを作れる
分解(デコンストラクト)各プロパティを変数に取り出せる
var moved = a with { X = 10 };   // Point { X = 10, Y = 2 }
var (x, y) = a;                   // x = 1, y = 2
csharp
record は「値そのものを表すデータ」に使い、class は「振る舞いや状態を持つオブジェクト」に使う、という切り分けが基本です。
使うもの向いている対象
recordAPIのレスポンス、設定値、座標、金額、検索条件
classサービス、リポジトリ、状態を持ち回るオブジェクト
前に進むイメージ
recordの登場で、データを表すクラスの記述量は大きく減りました

タプル — 名前を付けずに複数の値を返す

複数の値を返したいだけなら、クラスを作らずタプルで済ませられます。

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);   // 分解して受け取る
csharp

要素に名前を付けられるため、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",
    _ => "その他"
};
csharp

int n のように、判定と同時に変数へ取り出せます。第7章 の is と同じ仕組みです。

プロパティパターン

オブジェクトの中身の値で分岐できます。

string GetShipping(Order order) => order switch
{
    { Total: > 10000 } => "送料無料",
    { Address.Prefecture: "沖縄県" } => "離島料金",
    { IsExpress: true } => "速達",
    _ => "通常配送"
};
csharp

{ Address.Prefecture: "沖縄県" } のように、入れ子のプロパティも直接書けます。

リレーショナルパターンと論理パターン

string Judge(int score) => score switch
{
    < 0 or > 100 => "不正な値",
    >= 90 => "S",
    >= 70 and < 90 => "A",
    _ => "B"
};
csharp

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})",
    _ => "その他"
};
csharp
record とパターンマッチングは組み合わせて使うことで力を発揮します。データを record で表し、switch 式で形に応じて処理を決める、という書き方がモダンなC#の定番です。

nullの判定

if (user is null) { }
if (user is not null) { }
csharp

== null より意図が明確で、演算子が上書きされている型でも確実に動きます。第10章 で扱ったnullチェックでも、この書き方が推奨されます。

よくあるハマりどころ

recordの中に可変なコレクションを入れる

public record Basket(List<string> Items);
csharp

record 自体は等価比較を提供しますが、List は参照型なので中身が同じでも別インスタンスなら等しくなりません。加えて、外から中身を変更できてしまいます。不変にしたいなら IReadOnlyList<string> を使い、生成時に確定させます。

switch式で _ を書かない

どのパターンにも一致しなかった場合、実行時に例外が発生します。enum を網羅したつもりでも、将来値が追加されると漏れます。_ は必ず用意してください。

パターンを詰め込みすぎる

1つの switch 式に10個以上の分岐や複雑な条件を並べると、かえって読みにくくなります。判定の意味ごとにメソッドへ切り出すほうが読み手に親切です。

先生先生

新しい構文は「短く書ける」より「意図が伝わる」ときに使う。これは他の言語でも同じだね。

ちゃんと使うためのポイント

  • データを表す型は record、振る舞いを持つ型は class
  • with 式で「一部だけ違うコピー」を安全に作れる
  • 内部で複数の値を返すならタプル、公開APIなら record
  • switch 式は値・型・プロパティ・範囲で分岐でき、結果を1つの値として返す
  • nullの判定は is null / is not null
  • _ を必ず用意して、想定外の値で落ちないようにする

この本を読み終えたら

ここまでで、C#の言語としての基礎はひととおり押さえました。次に進む先は目的によって変わります。

言語の文法は入口にすぎません。実際に小さなアプリを1つ作り切ると、この本で扱った内容が一気につながります。

参考リンク