キーポイント
- C# 9 introduces records, a new reference type for encapsulating data developers can use instead of classes and structs.
- Record instances can have immutable properties through the use of pre-initialized positional parameters.
- Record types have a compiler-generated ToString method that returns the names and values of public properties and fields in an instance.
- Differently from classes, equality in Records doesn't necessarily mean reference equality. Two record instances are equal if the values of all their properties and fields are equal.
- Non-destructive mutation allows the creation of new record instances from existing immutable records.
- Records can be inherited.
レコードの導入
C#9ではレコードが導入されました。レコードは、データをカプセル化するための新しい参照型であり、開発者がクラスや構造体の代わりに使用できます。
レコードは変更可能ですが、この新しい参照型は主に不変データモデルで使用されることを目的としています。レコードには次の主要な機能があります。
- 不変プロパティを持つ参照型を作成するための位置指定構文
- 表示用の組み込み書式設定
- 値の等価性
- 非破壊的な変化の簡潔な構文
- 継承階層のサポート
レコード型の宣言構文は、declarationキーワードを除いて、クラスで使用される構文と非常によく似ています。
例1: レコードとクラスの宣言
public class Pet {
public string Name {get; set;}
public int Age{get; set;}
}
//キーワード"class"を"record"に切り替える
public record Pet {
public string Name {get; set;}
public int Age{get; set;}
}
上記の例は、名前による作成によって従来のゲッタセッタを持つPet
レコードを宣言する方法を示しています。
変更可能なプロパティ
レコードは主に不変のデータモデルで使用することを意図していますが、必ずしも不変ではありません。
上記の例では、 set
アクセサでレコードプロパティを宣言しました。つまり、オブジェクトの状態は、オブジェクトの作成後に変更可能にして修正できます。ただし、位置パラメタを使用してレコード型を宣言すると、デフォルトで不変になります。
例2: 位置パラメタを使用した宣言
public record Pet(string Name, int Age);
不変性は、初期化後にレコード変数への変更を許可したくない特定のシナリオで役立ちます。データ転送オブジェクト(DTO)は、不変性を使用するすばらしい例でしょう。不変レコードをDTOとして使用することで、オブジェクトがデータベースとクライアント間で転送される時に、変更されないようにします。
不変レコード型はスレッドセーフであり、作成後に変更または変化することはできませんが、非破壊的な変更は可能です。レコード型はコンストラクタ内でのみ初期化できます。
Init専用セッタ
Init専用セッタはC#9で導入され、プロパティとインデクサのset
の代わりに使用することができます init
アクセサは二つの例外を除いてreadonly
と非常に似ています。
- Init専用セッタを持つプロパティは、オブジェクト初期化子、コンストラクタ、またはinitアクセサにだけ設定できます。
- 一度、値が設定されると、変更できません。
つまり, init
は、レコードの状態を変更するためのウィンドウを提供しますが、init専用セッタを使用するプロパティは、レコードが初期化されると読み取り専用になります。
例3: 名前レコード宣言でinit専用セッタを使用する
public record Pet {
public string Name {get; init;}
public int Age{get; init;}
}
Init専用セッタを使用する最大の利点の1つは、メソッドへ参照するobject引数を渡し、プロパティの値が何らかの理由で変更されることによって持ち込まれるバグを防げることです。
レコード型の使用
位置(コンストラクタ)パラメタを使用して、レコードのプロパティを初期化できます。
例4: 位置パラメタを使用したレコード型の宣言
public record Pet(string Name, int Age);
上記の例では、 Pet
レコード型のためにプロパティのName
と Age
の位置(コンストラクタ)パラメタを使用します。ただし、レコードの初期化中に他のプロパティを必ずしも設定する必要がない場合、名前宣言と位置宣言を組み合わせることもできます。
例5: レコードで名前宣言と位置宣言を使用する
public record Pet(string Name, int Age)
{
public string Color{ get; init; }
}
上記の例では、プロパティの Color
は位置パラメタとして宣言されていません。その結果、 Pet
の新しいインスタンスを作成する時に、Colorを設定する必要がありません。
例6: 名前宣言と位置宣言でレコード型を初期化する
var dog = new Pet("Cookie", 7);
var dog = new Pet("Cookie", 7){Color = “Brown”};
表示用の組み込み書式設定
クラスとは異なり、コンパイラで生成されたレコード ToString()
メソッドは、 StringBuilder
を使い、プロパティとフィールドがpublicの場合にインスタンスの名前と値を表示します。
例7: レコード型で ToString()
を使う
var dog = new Pet("Cookie", 7){Color = “Brown”};
dog.ToString();
出力:
Pet{ Name = Cookie, Age= 7, Color = Brown}
値の等価性
値の等価性とは、型定義が同一であり、すべてのフィールドで両方のレコードの値が等しい場合に、レコード型の2つの変数が等しいことを意味します。
反対に、クラス型の2つの変数は、すべてのプロパティが同じであっても、等しくないと見なされます。これは、クラスが 参照等式 を使用するという事実によるものです。つまり、2つの変数は同じオブジェクトを参照する場合のみ等しいということです。
例8:同じレコード型の2つの異なるインスタンスを比較する
var pet1 = new Pet("Cookie", "7");
var pet2 = new Pet("Cookie", "7");
var areEqual = pet1.Equals(pet2);
上記の areEqual
状態は、C#のクラス変数の場合、 異なるオブジェクトを指すため、false を返します。しかし、レコード変数の場合、それぞれのプロパティで同じ型の同じ値を保持するため、 true を返します。
ただし、変数が2つの異なるレコード型を参照する場合、状態は falseを返します。
例9: レコードの2つの異なる型を作成する
public record Pet(string Name, int Age);
public record Dog(int Age, string Name): Pet(Name, Age);
Pet pet = new Pet("Cookie", 7);
Dog dog = new Dog(7, "Cookie");
var areEqual = pet.Equals(dog);
この場合、 areEqual
状態は、 Dog
と Pet
が同じレコード型ではないため、 false を返します。
非破壊的な変化
独自の定義により、不変レコード型は初期化した後で変更できません。例として、init専用セッタで不変のレコード型を使用してみましょう:
例10: init専用セッタを使用して不変レコード型を宣言する
public record Pet
{
public string Name{ get; init; }
public int Age{ get; init; }
};
var newPet = pet;
newPet.Name = "Cookie";
newPet.Age = 7;
上記のPetは、すべてのプロパティにinit専用セッタがあるため実行されません。 newPet
のプロパティの値は、変数が初期化された後は変更できません。その結果、newPet
のプロパティの値は設定されません。
既に初期化されている不変のレコードインスタンスのプロパティを変更できるようにするには、新しいrecordインスタンスを作成し、初期化中にプロパティを修正する必要があります。このプロセスは、 非破壊的変化と呼ばれます:
例11: 既存のレコード変数のプロパティを変更する
var pet = new Pet("Cookie", 7)
{
Color = "Brown"
};
var modifiedPet = new Pet(pet.Name, pet.Age)
{
Color = "Black"
};
上記の例では、非破壊的な変化を使い、 pet
変数のプロパティを変更しました。インスタンス化するプロセスの間に、 Color
プロパティの値を変更し、pet
をベースに modifiedPet
という新しい変数を作成します。
新しい変数を作成する時に変更したいプロパティのみを指定するために、 with
式を使用することもできます。
例12: with
を使った非破壊的変化
public record Pet(string Name, int Age);
Pet pet = new Pet("Cookie", 7);
var modifiedPet = Pet with
{
Age = 10
};
上記の例では、 with
式を使って、pet
のコピーを作成し、プロパティAgeの値を変更します。他のすべてのプロパティ値は、 pet
からコピーされます。with
式は、C#9ではレコード型でのみ使用できることにも注意してください。
同様に、既存のレコードをコピーするために、 with
の異なる構文を使用することもできます。
例13: with
を使用して、レコード変数をコピーする
var pet1 = new Pet("Cookie", 7);
var pet2 = pet1 with{};
var areEqual = pet1.Equals(pet2);
継承
レコードは別のレコードから継承できます。ただし、レコードはクラスから継承できず、クラスはレコードから継承できません。
例14: レコード型の継承
public record Pet(string Name, int Age);
public record Dog(string Name, int Age, string Color): Pet(Name, Age);
var dog = new Dog("Cookie", 7, “Brown”);
上記の例では, Dog
は Pet
レコード型から継承されるレコード型です。
デコンストラクタの使用
レコードは、レコードインスタンスをすべてのプロパティを含むタプルに変換するデコンストラクタもサポートしています。以下の例では Dog(dog)
のインスタンスを作成します。それは Pet
から継承されていますが、異なる順序でプロパティを持ちます。
例15: 継承された Dog
インスタンスを作成する
public record Pet(string Name, string Color);
public record Dog(string Color, string Name): Pet(Name, Color);
var dog = new Dog(“Brown”, "Cookie");
位置構文を使用した結果を示すために、継承する間にプロパティの順序を変更しました。変数を分解することにより、その値が分解されるインスタンスの型に依存するタプルが返ります。
例16: Pet
にキャストした Dog
のインスタンスを分解する
string name = null;
string color = null;
(name, color) = (Pet)dog;
Console.WriteLine($"{name} {color}");
出力:
Cookie Brown
上記の例では、 Pet
のインスタンスとしてキャストした Dog
を分解しています。その結果、分解は 同じ順序で Pet
型で宣言されたプロパティ値を返します。
Dog
を Pet
でキャストしない場合、分解プロセスはプロパティ値を異なる順序で返します。(レコード型 Dog
の宣言の次に)
例17: Dog
インスタンスの分解
(color, name) = dog;
Console.WriteLine($"{name} {color}");
出力:
Cookie Brown
レコード型の有利なケース:
ケース1: initセッタをバイパスする
不変性は、データ中心の型としてレコードを使用する利点の1つです。しかし、他のオブジェクトと同様にリフレクションを使って実行している時に、レコードインスタンスのプロパティ値を変更することは可能です。これにより、開発者はレコード宣言中に使用されるinit専用セッタをバイパスすることができます。
例18: リフレクションを使用したinitセッタのバイパス
public record Pet(string Name, int Age)
{
public string Color {get; init;}
};
Pet pet = new Pet("Cookie", 7)
{
Color = “Brown”
};
var propertyInfo = typeof(Pet).GetProperties()
.FirstOrDefault(p => p.Name == nameof(pet.Color));
propertyInfo.SetValue(pet, “Black”);
Console.WriteLine(pet.Color);
出力:
Black
上記の例は、初期化された後でも、不変レコード変数の Color
プロパティの値を修正するために GetProperties()
を使用する方法を示しています。
ケース2: シングルパラメタレコードを分解する
レコード変数を分解する場合、レコード型に少なくとも2つの位置パラメタ(プロパティ)が必要です。
例19:位置パラメタ1つだけでレコードを分解する
public record Pet(string Name);
Pet pet = new Pet("Cookie");
String name = “Something”;
(name) = pet
上記のコードスニペットは動かないでしょう。コンパイラは、 String
変数に Pet
オブジェクトを割り当てようとしていると理解するからです(括弧付きであっても (name)
は、シングルパラメタタプルとして解釈されません)。別の方法として、デコンストラクタ出力を明示的に定義できます。
public record Pet(string Name);
Pet pet = new Pet("Cookie");
String name = “Something”;
pet.Deconstruct(out name);
いつレコード型を使うべきか?
以下の場合に、アプリケーションでレコード型を使うべきです。
- データ型が複雑な値をカプセル化する。
- アプリケーションの他の部分に転送できる唯一の方法である(単方向データフロー)。
- 継承階層を使用する必要がある。
データ型が独自のメモリ割り当て内にデータを保持する値型になる可能性があり(例: int、double
)、不変である必要がある場合、構造体を使うべきです。構造体 は値型です。クラス は参照型であり、 レコード はデフォルトで不変の参照型です。
レコード型を使用する利点:
- 不変オブジェクトの状態は初期化されると変更されないため、レコード型はメモリ管理プロセスを助け、コードの保守とデバッグを容易にします。
- レコード型は、初期の段階でデータ関連のバグに気づくのに役立ち、構文の拡張のためにコードベースは(通常)はるかに小さくてクリーンになります。
レコード型の欠点:
IComparable
はサポートしていません。これは、レコード型のリストがソートできないことを意味します。この制限の例として、次のコードは例外をスローします。
例20: レコード型の異なるインスタンスをソートする
var pet1 = new Pet("Cookie", "7");
var pet2 = new Pet("Cookie", "8");
var list = new List<Pet>
{
pet1, pet2
};
list.Sort();
著者について
Tugce Ozdeger 氏は、Uppsala大学でコンピュータサイエンスの修士号を取得し、スウェーデンのストックホルムを拠点に、シニアソフトウェアエンジニアとして、.NET Frameworkの10年以上の専門的な実務経験を持っています。彼女は、主に、C#.NET、デスクトップアプリケーション(WinForms、WPF)、WCF、LINQ、.NET Core、SQL Server、Web Forms、ASP.NET MVC、EF Core、MVVMを専門とし、AIだけでなくMicrosoft Azureにも真の関心を持っています。また、Heart-Centric Tech Mentoringの創設者およびテック業界の女性のためのキャリアを加速させるメンタであり、現在、CTOとしてテックスタートアップを共同設立しています。