はじめに
Deep C# vol.1 – await foreaach に続く記事となります。
C# 9.0/10.0 でそれぞれ record / record struct
が追加されました。
この拡張がプログラマーにもたらすメリットを理解するために、これらがどのようなコードに展開されるか調べるのは有益です。
従来のclassと比べてみましょう。
class / record / record struct を記述してデコンパイルする
書き方は簡単です。必要なpublicプロパティを作りましょう。今回はset
ではなくinit
を用いています。この理由は後述します。
class についても比較のために同様に記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Class { public int Value { get; init; } } public record Record { public int Value { get; init; } } public record struct RecordStruct { public int Value { get; init; } } |
コンパイル後にデコンパイルすると、少し長いですがこのようなコードとなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
public class Class { public int Value { get; set/*init*/; } } public class Record : IEquatable<Record> { [CompilerGenerated] protected virtual Type EqualityContract { [CompilerGenerated] get { return typeof(Record); } } public int Value { get; set/*init*/; } [CompilerGenerated] public override string ToString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Record"); stringBuilder.Append(" { "); if (PrintMembers(stringBuilder)) { stringBuilder.Append(' '); } stringBuilder.Append('}'); return stringBuilder.ToString(); } [CompilerGenerated] protected virtual bool PrintMembers(StringBuilder builder) { RuntimeHelpers.EnsureSufficientExecutionStack(); builder.Append("Value = "); builder.Append(Value.ToString()); return true; } [CompilerGenerated] public static bool operator !=(Record? left, Record? right) { return !(left == right); } [CompilerGenerated] public static bool operator ==(Record? left, Record? right) { return (object)left == right || (left?.Equals(right) ?? false); } [CompilerGenerated] public override int GetHashCode() { return EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(Value); } [CompilerGenerated] public override bool Equals(object? obj) { return Equals(obj as Record); } [CompilerGenerated] public virtual bool Equals(Record? other) { return (object)this == other || ((object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<int>.Default.Equals(Value, other!.Value)); } [CompilerGenerated] protected Record(Record original) { Value = original.Value; } public Record() { } } public struct RecordStruct : IEquatable<RecordStruct> { public int Value { get; set/*init*/; } [CompilerGenerated] public override readonly string ToString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("RecordStruct"); stringBuilder.Append(" { "); if (PrintMembers(stringBuilder)) { stringBuilder.Append(' '); } stringBuilder.Append('}'); return stringBuilder.ToString(); } [CompilerGenerated] private readonly bool PrintMembers(StringBuilder builder) { builder.Append("Value = "); builder.Append(Value.ToString()); return true; } [CompilerGenerated] public static bool operator !=(RecordStruct left, RecordStruct right) { return !(left == right); } [CompilerGenerated] public static bool operator ==(RecordStruct left, RecordStruct right) { return left.Equals(right); } [CompilerGenerated] public override readonly int GetHashCode() { return EqualityComparer<int>.Default.GetHashCode(Value); } [CompilerGenerated] public override readonly bool Equals(object obj) { return obj is RecordStruct && Equals((RecordStruct)obj); } [CompilerGenerated] public readonly bool Equals(RecordStruct other) { return EqualityComparer<int>.Default.Equals(Value, other.Value); } } |
class と record の違い
classのコンパイル結果では、C#としては何も生成されていません。
隠れた実装としてクラス内のすべてを0で初期化する、コンストラクター前の事前動作が自動的に組み込まれますが、それはコード上には現れません。
recordは、以下の機能が追加されていることが分かります。
EqualityContract
という型比較用のプロパティを自動生成するIEquatable<T>
を実装して等値比較のEquals()
とoperator ==
operator !=
を自動生成する- publicプロパティを出力する
ToString()
を自動生成する GetHashCode()
を自動生成する- protectedなコピーコンストラクターを自動生成する
class は従来の参照型ですが、 record は参照型でありながら内部値の比較を可能にしたクラスであることが分かります。
また、コピーコンストラクターを持っていることから、クラスの内容の複製も容易です。
この機構を利用すれば、record で定義されたものはすべてDeep Copy対象となります。プログラマーが Deep Copyを丁寧に実装する必要はもうなくなりました。
record / record struct で init を推奨する理由
プロパティの set の代わりに init を用いれば、コンストラクター内とwith式を用いたプロパティの設定以降、値の更新を禁止することが可能です。
1 2 3 4 |
// with式の例 var foo = new Record { Value = 1 }; foo1.Value = 100; // NG : Valueはsetの代わりにinitを用いているためErrorとなります。 |
ここから私たちが享受するメリットは「すべてのpublicプロパティを init とした上で自動生成されるrecordは、安全性と保守性が高い」ことです。
実際のユースケースとして、あるrecordをDictionaryやHashSetに登録することを考えてみましょう。
プロパティを更新可能にしていると、登録後にハッシュ値が変わる可能性があります。
プロパティのsetterを init にすれば、そのプロパティは変更不可能にすることができ、ハッシュ値は不変値となります。また等値比較メソッドがあるので、インスタンスではなく内部値の比較で一意性を確認することが出来ます。
加えてこのコードは保守する必要がありません。プロパティを変更すれば自動的に追従します。
これは極めて大きなメリットです。
record / record structに共通の機能と差異
共通点は以下の通りです。
IEquatable<T>
を実装して等値比較のEquals()
とoperator ==
operator !=
を自動生成する- publicプロパティを出力する
ToString()
を自動生成する GetHashCode()
を自動生成する
汎用的なコードでありながら、比較時とハッシュコード生成時にはEqualityComparer<T>.Default
を用いたパフォーマンス配慮があるのは良い点です。
最速のコードではありませんが、box化によるコストはありません。
また、IEquatable<T>
を実装して等値比較は内部値の比較であることが明確であるのも良いと感じます。
差異については簡単で、まずrecord
はクラスベースでrecord sctuct
は構造体ベースです。
クラスの方は型比較のためのプロパティと、protectedなコピーコンストラクタが準備されています。(構造体は継承できないため型比較は必要ありません)
使いどころ
ここまで見たことを踏まえると、私たちが積極的にrecord / record sctuctとinitを使うべきケースがいくつか考えられます。
- データベースのEntityの型や通信に用いる型
- ValueObjectの簡素な実装
- 実装不備によって破壊されない安全な辞書やハッシュテーブルの生成
- 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
を扱います。