1. はじめに
ポジショナル構文のrecordクラスは、シンプルなケースには本当に便利だよね:
public record User(string Name, int Age);
でも、たまにrecordに追加のメソッドやカスタムプロパティ、アクセス修飾子の変更、コンストラクタにロジック(例えばバリデーションや「自動」データ変換)を入れたくなることがあるよね。残念ながら、ポジショナル構文だとそういうのを追加する場所がない!そんなときは明示的なボディ構文に切り替えよう。例えば:
- recordをメソッドやプロパティ、追加ロジックで拡張したいとき。
- プロパティの挙動(setter、getter、init、バリデーション)をコントロールしたいとき。
- 初期化方法ごとに複数のコンストラクタを作りたいとき。
- インターフェースを追加したり、特別なメソッドを実装したいとき。
recordのボディを作る
構文はクラスとほぼ同じ。こんな形、見覚えあるでしょ:
public class User
{
/* ... */
}
でも今度はrecordだよ:
public record User
{
// 明示的に定義したプロパティ
public string Name { get; init; }
public int Age { get; init; }
// 追加ロジック
public string GetGreeting()
{
return $"やっほー、俺の名前は{Name}、年齢は{Age}歳だよ!";
}
// カスタムコンストラクタ
public User(string name, int age)
{
Name = name;
if (age < 0)
throw new ArgumentException("年齢はマイナスにできないよ!");
Age = age;
}
}
豆知識:プロパティを自分で定義した場合、コンパイラはポジショナル構文から自動でプロパティを作ってくれない。全部自分でコントロールできるってこと!
ミックス型:ハイブリッド構文
C#は両方の世界をミックスできる:ポジショナルrecordを宣言して、さらにボディを追加できるよ:
public record User(string Name, int Age)
{
public string GetGreeting()
{
return $"俺は{Name}、{Age}歳だよ!";
}
}
この場合、NameとAgeのプロパティはポジショナル構文で自動生成されて、追加メソッドはボディの中に書けるよ。
2. 普通のrecordとの違い ― ボディの細かい話
- 明示的なrecordなら、コンストラクタ・プロパティ・メソッドを完全にコントロールできる。
- インターフェース実装や、複雑なオブジェクト識別ルールのための独自比較ロジックも追加できる。
- ポジショナル構文のrecordと違って、新しいプロパティを追加したいときはボディに宣言するだけでOK。
// 初心者のミス!
public record User(string Name, int Age)
{
public string Name { get; init; } // ← コンフリクト! プロパティの二重宣言
}
初心者のミス:一部の学生はpublic record User(string Name, int Age)と書きつつ、ボディにもpublic string Name { get; init; }みたいにプロパティを追加しようとするけど、これは別の変数じゃなくて同じプロパティの二重宣言だからコンフリクトになるよ。完全にポジショナル構文を使うか、全部明示的にプロパティを書くか、どっちかにしよう!
実践例
今、コンソールアプリを作ってて、ユーザーが注文を作るっていうシナリオを考えよう。注文クラスOrderは今までこんな感じだった:
public record Order(string Product, int Quantity, double Price);
今度は数量(quantityは1以上じゃないとダメ)をバリデーションしたいし、合計金額のプロパティも欲しいとする:
public record Order
{
public string Product { get; init; }
public int Quantity { get; init; }
public double Price { get; init; }
public double TotalCost => Quantity * Price;
public Order(string product, int quantity, double price)
{
Product = product ?? throw new ArgumentNullException(nameof(product));
if (quantity < 1)
throw new ArgumentException("数量は1以上じゃないとダメだよ!");
Quantity = quantity;
Price = price;
}
public override string ToString()
=> $"商品: {Product}, 数量: {Quantity}, 合計: {TotalCost}";
}
ここではプロパティを明示的に実装して、initセッター(イミュータブルなオブジェクト)にして、自動で合計金額を計算するプロパティも追加したよ ― コードが柔軟になった!
プログラムでの呼び出し例:
var order = new Order("自転車", 2, 15000);
Console.WriteLine(order); // 商品: 自転車, 数量: 2, 合計: 30000
3. record struct
structの進化
record structが登場する前、C#のstructは「シンプルな作業馬」だった ― コピーが速くて、スタックに置かれて、座標や色みたいな短い“Parcel”データにピッタリ。でもrecordの便利機能はなかった:ポジショナル構文もwith式も、デフォルトの値比較も、その他の“おいしい機能”もなかったんだ。
今はC#でrecordスタイルのstructが宣言できるようになった:
public record struct Point(int X, int Y);
で、これ何が嬉しいの?
- Equals、GetHashCode、ToStringが自動実装 ― つまりstructでもきれいに比較・表示できる!
- withクローン構文:var p2 = p1 with { X = 10 };
- ポジショナル構文も明示的な構文も使える。
比較:クラシックstruct VS record struct
| struct | record struct | |
|---|---|---|
| ポジショナル構文 | なし | あり |
| with式 | なし | あり |
| イミュータブル性 | なし(デフォルト) | あり(init) |
| 値比較 | なし(デフォルト) | あり |
| ToString | 標準 | きれい |
record structの明示的なボディ構文
普通のrecordと同じだけど、structバージョン:
public record struct Rectangle
{
public int Width { get; init; }
public int Height { get; init; }
public int Area => Width * Height;
public Rectangle(int width, int height)
{
Width = width > 0 ? width : throw new ArgumentException("幅は0より大きくしてね");
Height = height > 0 ? height : throw new ArgumentException("高さは0より大きくしてね");
}
public void Print()
{
Console.WriteLine($"サイズ: {Width} x {Height}, 面積: {Area}");
}
}
使い方例:
var rect = new Rectangle(10, 7);
rect.Print(); // サイズ: 10 x 7, 面積: 70
// クローンして幅だけ変えて、元はそのまま
var wideRect = rect with { Width = 20 };
wideRect.Print(); // サイズ: 20 x 7, 面積: 140
record structの特徴
- これもstruct ― value type。代入でコピーされるよ!
- recordのメリット全部:値比較、withクローン、きれいなToString。
- 小さくてコンパクトなイミュータブルデータセットで、ヒープアロケーションを避けたいときにおすすめ。
- ポジショナルパラメータも、明示的なボディもどっちもOK。
4. よくあるミスと落とし穴
無駄なミュータブル性: record structでも、普通のフィールド(例:public int Value;)にしちゃうと自動でイミュータブルにはならないよ。initセッターを使って、本当にイミュータブルなstructにしよう!
比較: 新しいフィールドを手動で追加した場合(ポジショナル構文に含めてないやつ)、値比較に参加するのはコンストラクタやinitセッター付きのフィールドだけだよ。
コピー: これはstructだから…全部コピーされる!参照型recordと混同しないように。
with式の混乱: これは常にshallow copy(浅いコピー)だから、ネストしたオブジェクトのdeep copyにはならないよ。
GO TO FULL VERSION