はじめに
C# 9.0 でレコード型が、C# 10.0 で値型レコードが導入されました。今回は値型レコードを逆コンパイルしてみて、どのような実装になっているか見ていこうと思います。
なお参照型レコードは継承が絡むため、値型レコードよりやや複雑です。
追記:ハッシュコードのマジックナンバー -1521134295
について
解説しているブログによると、素数とかをあれして衝突しにくいハッシュコードにしているようです。
逆コンパイル結果
record struct Person(string FirstName, string LastName, int Age);
↓
[NullableContext(1)]
[Nullable(0)]
internal struct Person : IEquatable<Person>
{
public Person(string FirstName, string LastName, int Age)
{
this.FirstName = FirstName;
this.LastName = LastName;
this.Age = Age;
}
// 書き換え可能なプロパティです
// readonly get になっている点が細かい
public string FirstName { readonly get; set; }
public string LastName { readonly get; set; }
public int Age { readonly get; set; }
[NullableContext(0)]
[CompilerGenerated]
public override readonly string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Person");
stringBuilder.Append(" { ");
if (this.PrintMembers(stringBuilder))
{
stringBuilder.Append(' ');
}
stringBuilder.Append('}');
return stringBuilder.ToString();
}
// このメソッドはデバッグ用?
[NullableContext(0)]
[CompilerGenerated]
private readonly bool PrintMembers(StringBuilder builder)
{
builder.Append("FirstName = ");
builder.Append(this.FirstName);
builder.Append(", LastName = ");
builder.Append(this.LastName);
builder.Append(", Age = ");
builder.Append(this.Age.ToString());
return true;
}
[CompilerGenerated]
public static bool operator !=(Person left, Person right)
{
// !left.Equals(right) のほうが
// メソッド呼び出しを1回減らせるような気がするものの
// こっちのほうが意図が明確です
return !(left == right);
}
[CompilerGenerated]
public static bool operator ==(Person left, Person right)
{
return left.Equals(right);
}
[CompilerGenerated]
public override readonly int GetHashCode()
{
// バッキングフィールドに直接アクセスしている!
// マジックナンバー -1521134295
return (EqualityComparer<string>.Default.GetHashCode(this.<FirstName>k__BackingField) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.<LastName>k__BackingField)) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(this.<Age>k__BackingField);
}
[NullableContext(0)]
[CompilerGenerated]
public override readonly bool Equals(object obj)
{
// obj is Person value && this.Equals(value); は
// is とキャストを1回で済ませてお得な気がするものの
// JIT 的には同じなのかも
return obj is Person && this.Equals((Person)obj);
}
[CompilerGenerated]
public readonly bool Equals(Person other)
{
return EqualityComparer<string>.Default.Equals(this.<FirstName>k__BackingField, other.<FirstName>k__BackingField) && EqualityComparer<string>.Default.Equals(this.<LastName>k__BackingField, other.<LastName>k__BackingField) && EqualityComparer<int>.Default.Equals(this.<Age>k__BackingField, other.<Age>k__BackingField);
}
[CompilerGenerated]
public readonly void Deconstruct(out string FirstName, out string LastName, out int Age)
{
FirstName = this.FirstName;
LastName = this.LastName;
Age = this.Age;
}
}
かゆいところ
-
readonly
修飾- 値を書き換えないメソッドに
readonly
修飾がついています - 「隠れたコピー」(hidden copy) を回避でき、パフォーマンスがよくなります
- 値を書き換えないメソッドに
- バッキングフィールドに直接アクセス
- 自動実装プロパティのバッキングフィールドに直接アクセスしているところがあります
- プロパティ経由の場所もあり、統一されていないようです
- 実行時プロパティ参照はだいたいインライン化されるため、パフォーマンスの違いはあまりなさそうです
- マジックナンバー
-1521134295
- ハッシュコードの計算にマジックナンバーを使っています
-
-1521134295
は検索してもよくわかりませんでした - 統計的にハッシュコードが衝突しにくくなるんですかね?
- 別のプロパティのハッシュコード計算にも同じマジックナンバーを使っています
- 16 進数: FFFF FFFF A555 5529
- ビット: 1111111111111111111111111111111110100101010101010101010100101001
- 排他的論理和とビットシフトに近いことをやっていそう?
- ビットシフトと違って桁が溢れても情報が失われないとか、そういうのかもしれません
値型タプルとの違い
値型レコードの実装は、基本的なところは値型タプルの実装と似ている気がします。
異なる点
-
IEquatable<T>
以外のインターフェイスの継承- 値型タプルは並び替えをサポートするようです
- 値をフィールドで公開するかプロパティで公開するか
- 値型タプルはフィールドで公開しています
- フィールドを参照渡しできるか(
ref
out
)で違いが出ます
-
GetHashCode()
の実装- 値型タプルはハッシュコードの計算に
HashCode.Combine()
を使用しています
- 値型タプルはハッシュコードの計算に
-
==
!=
演算子のサポート- 値型タプルではサポートされていません
おわりに
Equals()
等のボイラープレートコードの実装を、レコード型ならコンパイラに任せることができます。短いコードで表現しつつ多機能の恩恵を受けられるのがいいです。