はじめに
C# 9.0 でレコード型が、C# 10.0 で値型レコードが導入されました。
値型レコードについては前回の記事で掘り下げてみました(https://qiita.com/abetakahiro123/items/0ea0eeaa82e6921ede87)。今回は参照型レコードについて見ていきます。
☆レコード型の使用例
record Person(string Name, int Age);
record Customer(string Name, int Age, string Email) : Person(Name, Age);
var person1 = new Person("John", 30);
Assert.Equal("John", person1.Name);
Assert.Equal(30, person1.Age);
var person2 = new Person("John", 30);
Assert.Equal(person1, person2);
Assert.Equal(person1.GetHashCode(), person2.GetHashCode());
Assert.Equal((object)person1, (object)person2);
// プロパティのセッターは標準で init 属性が付与される
// person1.Age = 31; // エラー
// インスタンスの部分コピーは with 式を使うと便利
var person3 = person1 with { Age = 31 };
var customer1 = new Customer("John", 30, "a@example.com");
var customer2 = new Customer("John", 30, "a@example.com");
var customer3 = new Customer("John", 30, "a@example.com");
// Equals() は継承され、反射律・対称律・推移律を満たす
Assert.Equal(customer1, customer1);
Assert.NotEqual(person1, customer1);
Assert.NotEqual(customer1, person1);
Assert.Equal(customer1, customer2);
Assert.Equal(customer2, customer3);
Assert.Equal(customer1, customer3);
サンプルコード
テストコード
using Xunit;
file interface IPerson
{
string Name { get; }
int Age { get; }
}
file record Person(string Name, int Age) : IPerson;
file record Customer(string Name, int Age, string Email) : Person(Name, Age);
public class _RecordTest
{
[Fact]
void HowToUse()
{
var person1 = new Person("John", 30);
Assert.Equal("John", person1.Name);
Assert.Equal(30, person1.Age);
var person2 = new Person("John", 30);
Assert.Equal(person1, person2);
Assert.Equal(person1.GetHashCode(), person2.GetHashCode());
Assert.Equal((object)person1, (object)person2);
// プロパティのセッターは標準で init 属性が付与される
// person1.Age = 31; // エラー
// インスタンスの部分コピーは with 式を使うと便利
var person3 = person1 with { Age = 31 };
var customer1 = new Customer("John", 30, "a@example.com");
var customer2 = new Customer("John", 30, "a@example.com");
var customer3 = new Customer("John", 30, "a@example.com");
// Equals() は継承され、反射律・対称律・推移律を満たす
Assert.Equal(customer1, customer1);
Assert.NotEqual(person1, customer1);
Assert.NotEqual(customer1, person1);
Assert.Equal(customer1, customer2);
Assert.Equal(customer2, customer3);
Assert.Equal(customer1, customer3);
}
static void GetTypePerformance(Performance p)
{
var person = new Person("John", 30);
p.AddTest("GetType", () =>
{
var sum = 0;
for (int n = 0; n < 10000; ++n)
sum = p.GetType().GetHashCode();
});
p.AddTest("typeof", () =>
{
var sum = 0;
for (int n = 0; n < 10000; ++n)
sum = typeof(Performance).GetHashCode();
});
}
}
逆コンパイル結果
interface IPerson
{
string Name { get; }
int Age { get; }
}
record Person(string FirstName, string LastName, int Age) : IPerson;
↓
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
[NullableContext(1)]
[Nullable(0)]
internal class Person : IPerson, IEquatable<Person>
{
public Person(string Name, int Age)
{
this.Name = Name;
this.Age = Age;
base..ctor();
}
// Equals() で使用するための型情報取得用プロパティ
[CompilerGenerated]
protected virtual Type EqualityContract
{
[CompilerGenerated]
get
{
return typeof(Person);
}
}
public string Name { get; set; }
public int Age { get; set; }
[CompilerGenerated]
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Person");
stringBuilder.Append(" { ");
if (this.PrintMembers(stringBuilder))
{
stringBuilder.Append(' ');
}
stringBuilder.Append('}');
return stringBuilder.ToString();
}
[CompilerGenerated]
protected virtual bool PrintMembers(StringBuilder builder)
{
RuntimeHelpers.EnsureSufficientExecutionStack();
builder.Append("Name = ");
builder.Append(this.Name);
builder.Append(", Age = ");
builder.Append(this.Age.ToString());
return true;
}
[NullableContext(2)]
[CompilerGenerated]
public static bool operator !=(Person left, Person right)
{
// !left.Equals(right) のほうが
// メソッド呼び出しを1回減らせるような気がするものの
// こっちのほうが意図が明確です
return !(left == right);
}
[NullableContext(2)]
[CompilerGenerated]
public static bool operator ==(Person left, Person right)
{
// 参照型のためポインタ比較と null チェックあり
return left == right || (left != null && left.Equals(right));
}
[CompilerGenerated]
public override int GetHashCode()
{
// バッキングフィールドに直接アクセス
// マジックナンバー -1521134295 については後述
return (EqualityComparer<Type>.Default.GetHashCode(this.EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.<Name>k__BackingField)) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(this.<Age>k__BackingField);
}
[NullableContext(2)]
[CompilerGenerated]
public override bool Equals(object obj)
{
// ここは as による型変換で OK
return this.Equals(obj as Person);
}
[NullableContext(2)]
[CompilerGenerated]
public virtual bool Equals(Person other)
{
// EqualityContract による型比較については後述
return this == other || (other != null && this.EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(this.<Name>k__BackingField, other.<Name>k__BackingField) && EqualityComparer<int>.Default.Equals(this.<Age>k__BackingField, other.<Age>k__BackingField));
}
// 継承先でクローンをサポートするためのコピーコンストラクタ
[CompilerGenerated]
protected Person(Person original)
{
this.Name = original.<Name>k__BackingField;
this.Age = original.<Age>k__BackingField;
}
[CompilerGenerated]
public void Deconstruct(out string Name, out int Age)
{
Name = this.Name;
Age = this.Age;
}
}
気になるところ
-
EqualityContract
プロパティ-
virtual
にして継承することを前提としています。this.GetType()
でもいいような気がしますが、細かいカスタムの余地を残すのとパフォーマンス上の理由があるのかもしれません -
typeof(T)
は JIT コンパイル時定数だった気がするので、this.GetType()
より少しパフォーマンスが良いです(↓ の表を参照)
-
-
GetHashCode()
のマジックナンバー-1521134295
- 解説しているブログによると、素数とかをあれして衝突しにくいハッシュコードにしているようです。
-
Equals()
の実装-
Equals(object)
ではobj as Person
としていますが、ならいあるやり方だとthis.GetType()
による型チェックをします。Equals(Person)
で型チェックをするためこの実装になっています -
Equals(Person)
ではEqualityContract
プロパティによる型チェックをしています。これは同値性における対称律(a == b
->b == a
)を満たすために必要だったりします
-
this.GetType()
と typeof(T)
のパフォーマンス比較
Test | Score | % | CG0 |
---|---|---|---|
GetType | 2,398 | 100.0% | 0 |
typeof | 3,322 | 138.5% | 0 |
実行環境: Windows11 x64 .NET Runtime 9.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
- 少しだけ
typeof(T)
のほうがパフォーマンスが良さそうです
継承した場合
record Customer(string Name, int Age, string Email) : Person(Name, Age);
↓
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
[NullableContext(1)]
[Nullable(0)]
internal class Customer : Person, IEquatable<Customer>
{
public Customer(string Name, int Age, string Email)
{
this.Email = Email;
base..ctor(Name, Age);
}
[CompilerGenerated]
protected override Type EqualityContract
{
[CompilerGenerated]
get
{
// プロパティをオーバーライドしている
return typeof(Customer);
}
}
public string Email { get; set; }
[CompilerGenerated]
public override string ToString()
{
// 親クラスの PrintMembers() に連結
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Customer");
stringBuilder.Append(" { ");
if (this.PrintMembers(stringBuilder))
{
stringBuilder.Append(' ');
}
stringBuilder.Append('}');
return stringBuilder.ToString();
}
[CompilerGenerated]
protected override bool PrintMembers(StringBuilder builder)
{
if (base.PrintMembers(builder))
{
builder.Append(", ");
}
builder.Append("Email = ");
builder.Append(this.Email);
return true;
}
[NullableContext(2)]
[CompilerGenerated]
public static bool operator !=(Customer left, Customer right)
{
return !(left == right);
}
[NullableContext(2)]
[CompilerGenerated]
public static bool operator ==(Customer left, Customer right)
{
return left == right || (left != null && left.Equals(right));
}
[CompilerGenerated]
public override int GetHashCode()
{
return base.GetHashCode() * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.<Email>k__BackingField);
}
[NullableContext(2)]
[CompilerGenerated]
public override bool Equals(object obj)
{
return this.Equals(obj as Customer);
}
[NullableContext(2)]
[CompilerGenerated]
public sealed override bool Equals(Person other)
{
return this.Equals(other);
}
[NullableContext(2)]
[CompilerGenerated]
public virtual bool Equals(Customer other)
{
return this == other || (base.Equals(other) && EqualityComparer<string>.Default.Equals(this.<Email>k__BackingField, other.<Email>k__BackingField));
}
[CompilerGenerated]
protected Customer(Customer original) : base(original)
{
this.Email = original.<Email>k__BackingField;
}
// 親クラスの Deconstruct() に加えてオーバーロードを用意
[CompilerGenerated]
public void Deconstruct(out string Name, out int Age, out string Email)
{
Name = base.Name;
Age = base.Age;
Email = this.Email;
}
}
継承した場合の気になるところ
-
EqualityContract
プロパティのオーバーライド-
typeof(Customer)
になっています。手動だと面倒ですが、コンパイラ生成なのでパフォーマンスを優先できます
-
-
Deconstruct()
オーバーロードの追加-
Person.Deconstruct()
とCustomer.Deconstruct()
を使用できます。これによって柔軟に分解することができます
-
おわりに
書き換え不能の参照型データは使いたいことがあるため、短いコードで表現できる参照型レコードは強力です。
レコード型は基本的にデータを表現することに使うため、継承する場面はあまりなさそうです。もし継承が必要なときもコンパイラがうまいことやってくれるため、コードを書く人はかなり楽ができそうです。
以上の特性を踏まえて、値型レコードと参照型レコードをうまく使い分けたいです。