値型レコードを逆コンパイルしてみる

# はじめに

C# 9.0 で[レコード型](https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/record)が、C# 10.0 で値型レコードが導入されました。今回は値型レコードを逆コンパイルしてみて、どのような実装になっているか見ていこうと思います。

なお参照型レコードは継承が絡むため、値型レコードよりやや複雑です。

:::note info
追記：ハッシュコードのマジックナンバー `-1521134295` について
[解説しているブログ](https://blog.axantum.com/2021/04/what-are-weird-random-integer-constants.html)によると、素数とかをあれして衝突しにくいハッシュコードにしているようです。
:::

## 逆コンパイル結果

```cs
record struct Person(string FirstName, string LastName, int Age);
```

↓

```cs
[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) のほうが
		// メソッド呼び出しを１回減らせるような気がするものの
		// こっちのほうが意図が明確です
		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 とキャストを１回で済ませてお得な気がするものの
		// 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)](https://ufcpp.net/study/csharp/resource/readonlyness/#avoid-copy) を回避でき、パフォーマンスがよくなります
- バッキングフィールドに直接アクセス
  - 自動実装プロパティのバッキングフィールドに直接アクセスしているところがあります
  - プロパティ経由の場所もあり、統一されていないようです
  - 実行時プロパティ参照はだいたい[インライン化](https://ufcpp.net/study/csharp/structured/miscinlining/)されるため、パフォーマンスの違いはあまりなさそうです
- マジックナンバー `-1521134295`
  - ハッシュコードの計算にマジックナンバーを使っています
  - `-1521134295` は検索してもよくわかりませんでした
  - 統計的にハッシュコードが衝突しにくくなるんですかね？
  - 別のプロパティのハッシュコード計算にも同じマジックナンバーを使っています
  - 16 進数: FFFF FFFF A555 5529
  - ビット: 1111111111111111111111111111111110100101010101010101010100101001
  - 排他的論理和とビットシフトに近いことをやっていそう？
  - ビットシフトと違って桁が溢れても情報が失われないとか、そういうのかもしれません

## 値型タプルとの違い

値型レコードの実装は、基本的なところは[値型タプルの実装](https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/ValueTuple.cs#L629)と似ている気がします。

異なる点

- `IEquatable<T>` 以外のインターフェイスの継承
  - 値型タプルは並び替えをサポートするようです
- 値をフィールドで公開するかプロパティで公開するか
  - 値型タプルはフィールドで公開しています
  - フィールドを参照渡しできるか（`ref` `out`）で違いが出ます
- `GetHashCode()` の実装
  - 値型タプルはハッシュコードの計算に [`HashCode.Combine()`](https://learn.microsoft.com/ja-jp/dotnet/api/system.hashcode.combine?view=net-8.0) を使用しています
- `==` `!=` 演算子のサポート
  - 値型タプルではサポートされていません


## おわりに

`Equals()` 等の[ボイラープレートコード](https://ja.wikipedia.org/wiki/%E3%83%9C%E3%82%A4%E3%83%A9%E3%83%BC%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%B3%E3%83%BC%E3%83%89)の実装を、レコード型ならコンパイラに任せることができます。短いコードで表現しつつ多機能の恩恵を受けられるのがいいです。