前置き
来月(2020年11月)に、「.NET Framework」「.NET Core」を統合させた「.NET 5」がリリース予定です。
「.NET 5」ではC#も新バージョン「9.0」となり、新機能が幾つか追加されています。
その中の機能の一つとして、新たな型「レコード(record)」が追加されました。
「レコード」の概要は以下のように説明されています。
C# 9.0 には "レコード型" が導入されています。これは、等価性の値のセマンティクスを提供するための合成されたメソッドを提供する参照型です。 既定では、レコードは変更できません。
class と record の動作比較
※.NET 5のバージョンは「5.0.100-rc.2.20479.15」にて確認
LastNameとFirstNameというプロパティを持った「PersonClass」と「PersonRecord」というオブジェクトを定義し、
実行してみます。
public class PersonClass
{
public string LastName { get; }
public string FirstName { get; }
public PersonClass(string first, string last) => (FirstName, LastName) = (first, last);
}
public record PersonRecord
{
public string LastName { get; }
public string FirstName { get; }
public PersonRecord(string first, string last) => (FirstName, LastName) = (first, last);
}
var personClass1 = new PersonClass("Bill","Wagner");
var personClass2 = new PersonClass("Bill","Wagner");
Console.WriteLine(personClass1.Equals(personClass2));
Console.WriteLine(personClass1.ToString());
var personRecord1 = new PersonRecord("Bill","Wagner");
var personRecord2 = new PersonRecord("Bill","Wagner");
Console.WriteLine(personRecord1.Equals(personRecord2));
Console.WriteLine(personRecord1.ToString());
false // PersonClass同士のEqualsの結果
PersonClass // PersonClassのToString
true // PersonRecord同士のEqualsの結果
PersonRecord { LastName = Wagner, FirstName = Bill } // PersonRecordのToString
PersonClass同士のEqualsの結果はfalseです。
これはObject.Equalsの機能は既定では以下となっているからです。
参照型(class)の場合
二つの参照型オブジェクトが同一のオブジェクト(=参照先のオブジェクトが同一)である場合にtrue、そうでなければfalseを返す
オブジェクトが同一 = インスタンスが同じかどうかを比較するだけ、ということですね。
personClass1とpersonClass2はそれぞれnewされていて別のインスタンスなので、結果はfalseということです。
一方、PersonRecord同士は別のインスタンスとなっているにも関わらずEqualsの結果はtrueとなっています。
これは、前述の公式ドキュメントの記載のとおりです。
レコード定義によって、FirstName と LastName の 2 つの読み取り専用プロパティを含む Person 型が作成されます。 Person 型は参照型です。 IL を見た場合は、それはクラスです。 どのプロパティも作成後に変更できないので、それは変更できません。レコード型を定義すると、コンパイラによって他のいくつかのメソッドが自動的に合成されます。
- 値ベースの等価比較のためのメソッド
- GetHashCode() のオーバーライド
recordはコンパイルされるとclassとなり、定義されている全プロパティの値ベースの比較を行うEquals、GetHashCodeが自動で生成される、ということですね。
classと比べてrecordを使うと何がおトクなのかというのは、class(参照型)で値ベースの比較を行わせるようにするには、
自前でEqualsやGetHashCodeをoverrideする必要がありましたが、
recordはそれらを自動で実装してくれる=実装の手間が省けるということですね。
参考:型の値の等価性を定義する方法 (C# プログラミング ガイド)
また、classではToStringも既定ではそのclassの名称を出力するだけですが、recordではToStringもOverrideされてプロパティ名、その中の値も出力してくれるようになっています。
構造体(struct)との違い
構造体(struct)は値型なので、structのEqualsでも値ベースの比較を行ってくれます。
public struct PersonStruct
{
public string LastName { get; }
public string FirstName { get; }
public PersonStruct(string first, string last) => (FirstName, LastName) = (first, last);
}
var personStruct1 = new PersonStruct("Bill","Wagner");
var personStruct2 = new PersonStruct("Bill","Wagner");
Console.WriteLine(personStruct1.Equals(personStruct2));
true
ですが、前述通りrecordはコンパイルされるとclassとなるので、継承も使用することが出来ます。
レコードによって、継承がサポートされます。
構造体は継承を使用することは出来ません。
レコードはクラスの特性を持ちつつ値ベースの比較を行ってくれるということですね。
with式(2020/10/23追記)
※@Zuishin さん、コメントありがとうございました。
さらにレコード特有の機能「with式」により、一部のプロパティのみ値の違う複製オブジェクトを作る事が簡単に行えるようになりました。
var personRecord3 = personRecord2 with { FirstName = "Bob" };
with式を使う場合、こちらも9.0における新機能「init専用セッター」をプロパティに指定する必要があります。
↑
※@Zuishinさんに再びコメントで補足いただきました。
レコードの場合は以下のように宣言すると、デフォルトで init セッターが使用されます。
public record PersonRecord(string LastName, string FirstName);
参考:init 専用セッター
public class PersonClass
{
public string LastName { get; init; }
public string FirstName { get; init; }
}
レコードをどんな場面で使うか
想像ですが、ドメイン駆動開発におけるバリューオブジェクトに適用できるのではないかと考えています。
参考:.NETでドメイン駆動開発~ValueObject後編~
ValueObjectとは、
・・・
全プロパティ値が同じなら、同じオブジェクトである。例えオブジェクトのアドレスが異なっていても同じオブジェクト、として認識される。
他の言語でレコード型は存在するのか
Javaでは14で既に対応されていたようですね。
参考:Java14が出たので、とりあえずrecordを試してみた
※参考の記事を読んでしまうと、「C#のレコード型はJava14で追加されたRecordと同じです」で説明が済んでしまう気がしてきた・・・