Deep C# vol.2 – record struct

はじめに

Deep C# vol.1 – await foreaach に続く記事となります。
C# 9.0/10.0 でそれぞれ record / record struct が追加されました。
この拡張がプログラマーにもたらすメリットを理解するために、これらがどのようなコードに展開されるか調べるのは有益です。
従来のclassと比べてみましょう。

class / record / record struct を記述してデコンパイルする

書き方は簡単です。必要なpublicプロパティを作りましょう。今回はsetではなくinitを用いています。この理由は後述します。
class についても比較のために同様に記述します。

コンパイル後にデコンパイルすると、少し長いですがこのようなコードとなります。

class と record の違い

classのコンパイル結果では、C#としては何も生成されていません。
隠れた実装としてクラス内のすべてを0で初期化する、コンストラクター前の事前動作が自動的に組み込まれますが、それはコード上には現れません。
recordは、以下の機能が追加されていることが分かります。

  1. EqualityContractという型比較用のプロパティを自動生成する
  2. IEquatable<T>を実装して等値比較のEquals()operator ==operator !=を自動生成する
  3. publicプロパティを出力するToString()を自動生成する
  4. GetHashCode()を自動生成する
  5. protectedなコピーコンストラクターを自動生成する

class は従来の参照型ですが、 record は参照型でありながら内部値の比較を可能にしたクラスであることが分かります。
また、コピーコンストラクターを持っていることから、クラスの内容の複製も容易です。
この機構を利用すれば、record で定義されたものはすべてDeep Copy対象となります。プログラマーが Deep Copyを丁寧に実装する必要はもうなくなりました。

record / record struct で init を推奨する理由

プロパティの set の代わりに init を用いれば、コンストラクター内とwith式を用いたプロパティの設定以降、値の更新を禁止することが可能です。

ここから私たちが享受するメリットは「すべてのpublicプロパティを init とした上で自動生成されるrecordは、安全性と保守性が高い」ことです。

実際のユースケースとして、あるrecordをDictionaryやHashSetに登録することを考えてみましょう。
プロパティを更新可能にしていると、登録後にハッシュ値が変わる可能性があります。
プロパティのsetterを init にすれば、そのプロパティは変更不可能にすることができ、ハッシュ値は不変値となります。また等値比較メソッドがあるので、インスタンスではなく内部値の比較で一意性を確認することが出来ます。
加えてこのコードは保守する必要がありません。プロパティを変更すれば自動的に追従します。
これは極めて大きなメリットです。

record / record structに共通の機能と差異

共通点は以下の通りです。

  1. IEquatable<T>を実装して等値比較のEquals()operator ==operator !=を自動生成する
  2. publicプロパティを出力するToString()を自動生成する
  3. GetHashCode()を自動生成する

汎用的なコードでありながら、比較時とハッシュコード生成時にはEqualityComparer<T>.Defaultを用いたパフォーマンス配慮があるのは良い点です。
最速のコードではありませんが、box化によるコストはありません。
また、IEquatable<T>を実装して等値比較は内部値の比較であることが明確であるのも良いと感じます。

差異については簡単で、まずrecordはクラスベースでrecord sctuctは構造体ベースです。
クラスの方は型比較のためのプロパティと、protectedなコピーコンストラクタが準備されています。(構造体は継承できないため型比較は必要ありません)

使いどころ

ここまで見たことを踏まえると、私たちが積極的にrecord / record sctuctとinitを使うべきケースがいくつか考えられます。

  1. データベースのEntityの型や通信に用いる型
  2. ValueObjectの簡素な実装
  3. 実装不備によって破壊されない安全な辞書やハッシュテーブルの生成
  4. Deep Copyの自動的な実装

GetHashCode()を自動的に生成するので、特に 3 の用途を禁止していないケースではインスタンス生成後の内部値の書き換えは禁じるべきです。
そのため、すべてのpublicプロパティは set を用いず init を用いるべきでしょう。
自動生成コードの内容からして、record / record struct は immutable(書き換え不可) とする方が問題は発生しにくいことは明らかです。

record と record struct の使い分け

どちらも同等の機能を持ちます。決定的な差は等値比較の際に丸ごとコピーされるかどうかです。
record structは構造体なので、引数とする際に常にコピーされるコストがあります。
一般的に16bytes程度のサイズまでにおさまるならパフォーマンス面の問題はありません。
よって、物理的な単位をつけられる程度のものを表現するために用いるのは良いでしょう。(例えば重さ・長さなど)
recordはクラスベースなので参照が引き渡されるのみです。
この場合、生成時のヒープ領域確保とガーベージコレクションによる走査・回収時にコストが生じますが、コピーのコストは生じません。
どちらの形を用いるにしても、大きくなればただの比較でも相応のコストを持つようになることを覚えておくべきです。
しかしきわめて簡素に等値比較を実装できることは大きなメリットでもあります。

まとめ

record は class における一般的なユースケースを補完する優れた拡張となっていることが分かりました。
これはソースコード自動生成の本領発揮を感じさせてくれる見事な拡張です。
それと同時に、プロパティが変わる都度行ってきた厄介なメンテナンスからプログラマーを開放してくれるものともなっています。
そして、小さなものを record structで、大きなものは record で実装すべきというところもはっきりしています。この関係は struct と class の選択基準と全く変わりません。
これらを意識して使いこなしたいものです。

次回は await using を扱います。

最近の記事

  • 関連記事
  • おすすめ記事
  • 特集記事

アーカイブ

カテゴリー

PAGE TOP