1. データ受け渡しの問題
例えば、学校用のアプリがあって、生徒の情報(名前、生年、クラス)をいろんなモジュール間でやりとりしたいとする。普通どうする?
こんな感じでクラスを作るよね:
public class Student
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
見慣れたやり方。でもこの方法にはいくつか問題があるんだ:
- 2人の生徒を比較するとき、デフォルトだとオブジェクトの参照しか比較されない。つまり、フィールドが全部同じでも別オブジェクトなら「等しくない」ってなる!
- クラスは作成後に変更できちゃうから、どこかで使われてるオブジェクトをうっかり変えちゃうとバグの元になる;
- 「お決まりの」コードが多い:コンストラクタ、比較メソッド、コピー(クローン)メソッド、ToStringとか。
もう気づいたかもだけど、C#ならこういう面倒から解放される!登場するのがrecordだよ。
2. recordって何?
recordはC#でデータを保存するために特別に作られた型。recordには2つの大きな特徴がある:
- イミュータブル(immutability): record型のオブジェクトはデフォルトで不変、つまりプロパティの値は作成時に一度だけセットされて、後から変えられない(正確にはsetセッターがprivate)。もちろんmutableなrecordも作れるけど、基本はイミュータブル。
- 値による比較: 2つのrecordオブジェクトが全フィールドで同じ値なら、等しいとみなされる(==や.Equals()の挙動が違う!)。
実際、recordはアプリのレイヤー間でデータをやりとりするのにピッタリ(例えばDB→コントローラー、コントローラー→ビューとか)。
3. recordの構文
一番シンプルなのはポジショナル構文
値のセットを渡したいだけなら、1行で型を宣言できる:
public record Student(string Name, int YearOfBirth, string Class);
裏で何が起きてるかというと、コンパイラが自動で:
- 読み取り専用プロパティ(private setter付き)
- 全パラメータを受け取るコンストラクタ
- 比較・コピー用メソッド
- ちゃんと整形されたToString!
ポジショナルrecordの使い方
この新しい型を学校アプリで使ってみよう:
var student1 = new Student("イワン", 2008, "8A");
var student2 = new Student("マリア", 2008, "8B");
プロパティへのアクセスはいつも通り(ただし変更はできない):
Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");
作成後にプロパティを変更しようとすると
student1.Name = "ペトル"; // エラー!読み取り専用プロパティ。
この行をコメントアウト解除すると、コンパイラがすぐ怒るよ:「読み取り専用プロパティには値をセットできません」って。
自動生成されたToStringはこんな感じ
Console.WriteLine(student1); // 出力: Student { Name = イワン, YearOfBirth = 2008, Class = 8A }
手動でフォーマットしなくても、見やすくて分かりやすい!
4. recordオブジェクトの比較
おさらい:普通のクラスで同じデータのオブジェクトを2つ作っても、等しくはならない:
var a = new Student("イワン", 2008, "8A");
var b = new Student("イワン", 2008, "8A");
Console.WriteLine(a == b); // クラスの場合: false
でもStudentがrecordなら、期待通りに比較できる:
public record Student(string Name, int YearOfBirth, string Class);
var a = new Student("イワン", 2008, "8A");
var b = new Student("イワン", 2008, "8A");
Console.WriteLine(a == b); // recordの場合: true!
つまり、フィールドが全部同じなら、メモリ上で別オブジェクトでも等しいとみなされる。
5. 中身はどうなってる?
学生たちはよく、recordだとどれだけコンパイラが自動でやってくれるかに驚く。クラスで全部手書きした場合とrecordの違いを比べてみよう。
手書きの昔ながらのクラス
public class Student
{
public string Name { get; }
public int YearOfBirth { get; }
public string Class { get; }
public Student(string name, int yearOfBirth, string @class)
{
Name = name;
YearOfBirth = yearOfBirth;
Class = @class; //自分のクラスへの参照
}
public override bool Equals(object? obj)
{
if (obj is not Student other) return false;
return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, YearOfBirth, Class);
}
public override string ToString()
{
return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
}
}
そりゃプログラマーがパラノイアになるのも当然だよね、同じことを10回も書くんだから!
recordはたった1行
public record Student(string Name, int YearOfBirth, string Class);
6. recordとイミュータブル:できること・できないこと
recordはデフォルトでプロパティが読み取り専用だから、バグを防げて超便利。でもどうしても(例えば超古いAPI用とか)mutableなrecordが欲しいなら、こう書ける:
public record MutableStudent
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
これでフィールドを変更できるけど、一部のメリット(安全性とか)を失うので注意。
7. recordのデストラクチャリング
ポジショナル構文はタプルに似てるから、recordも簡単に分解できる:
var student = new Student("イワン", 2008, "8A");
var (name, year, className) = student;
Console.WriteLine($"{name} - {year}, {className}"); // イワン - 2008, 8A
コンパイラは各ポジショナルrecordにDeconstructメソッドを自動生成してくれるから、LINQやswitchパターンでも超便利。
8. recordはclass?それともstruct?
デフォルトだとrecordは参照型、つまりclassと同じ。参照型の特徴(ヒープに保存、参照コピーなど)がそのまま適用される。
値型(value type)が欲しい場合は、C#には専用の構文がある。こう書ける:
public record struct Point(int X, int Y);
でもデータの受け渡しには、ほとんどの場合クラシックなrecord(参照型)が使われる。record structについてはまた今度のレクチャーで :P
class、struct、recordの比較
| 型 | デフォルトでイミュータブル | 値による比較 | 簡単なデストラクチャリング | 自動ToString |
|---|---|---|---|---|
| class | いいえ | いいえ(参照比較) | いいえ | いいえ |
| struct | いいえ | はい | いいえ | いいえ |
| record | はい | はい | はい | はい |
9. 特徴とよくあるミス
初心者はrecordの使い方でよく混乱する。例えば、recordの1つのオブジェクトのフィールドを変えたら、もう1つのオブジェクトも変わる(参照コピーのクラスみたいに)と思いがち。違うよ! それに、withを使うときは、必ずコピーが作られて元のオブジェクトは変わらないって覚えておこう。recordはデータのクリーンさや予測可能性を大事にしたいロジックに最適。
あと、initでフィールドを宣言した場合も、setじゃなくて、作成時かwithでしか値をセットできないよ。
GO TO FULL VERSION