久々にC#を触ったら色々仕様が追加されていたので確認。
その中で record の様なデータを扱う場合の「プロパティ関連」で少し混乱したので、忘れないうちにメモ。
プロパティの書き方(おさらいと最新仕様)
1. フィールドとしてそのまま定義
public record class Foo
{
// これはプロパティではなく「フィールド」
public string Name = "";
}
2. プロパティ(アクセスを明記)
public record class Foo
{
public string Name { get; set; } = "";
}
3. fieldキーワード(C# 14 の新機能)
public record class Foo
{
public string Name
{
get => field;
set => field = value;
} = "";
}
用途の違い
1の「フィールド」と2の「プロパティ」は動きは似ていますが別物。
外部公開するデータは2のプロパティにするのがC#の基本方針(2はコンパイル時に隠しフィールドが自動生成)。
3の field キーワードは、C# 14 で導入された新構文。
setterの動作をカスタマイズしたい(値を検証したい等)場合等で、裏側に private string _name; のような変数を定義しなくても済むように。
「field」と「value」はgetやsetの中だけで使えるキーワード。
ザックリ、getでは field に保持されている値を返し、setでは value に入ってきた設定値を検証して field に入れる、と覚えておけばOKです。
public int NonMinusValue
{
get => field;
set => field = (value > 0) ? value : 0; // 0未満なら0にする
}
実際に使う時は?(イミュータブルな設計)
ただ、データを保存するクラスに対して「後から値を変更する(setする)」という処理は、減ってきているはず。
C#のような最近のフレームワークでは、既に確保されたインスタンスの値を書き換えるのではなく、 「新しい値を入れた別のインスタンスを生成して置き換える」 というのが、データのバグを減らすモダンな作法(イミュータブルな設計)らしい。
そのため、多分以下の様な init を使った記述が基本に。
public record class Foo
{
public string Name { get; init; } = "";
public int Index { get; init; } = 0;
}
init を付けると、インスタンスの生成時(初期化時)のみ値を設定できて、それ以降は readonly として振る舞う様に。
値を変更(更新)したい時は with 式を使う
var a = new Foo() { Index = 1, Name = "1" };
// : 何かの処理
// setterが無いので、これはコンパイルエラーになる
// a.Index = 2;
// a.Name = "2";
値を更新する時は?
従来なら a = new Foo() { Index = 2, Name = a.Name }; と手書きしていましたが、
record には with 式という便利な機能が。
var a = new Foo() { Index = 1, Name = "1" };
// : 何かの処理
// a の中身をコピーしつつ、Index だけ 2 に書き換えた「新しいインスタンス」を作る
a = a with { Index = 2 };
と言う感じで、元のデータ構造を壊さずに新しいインスタンスへと安全に更新していく感じになります。
細かい所はまだ足りてない気はするけど、取りあえずはこの辺りまでは理解。
おまけ:
上記のような init プロパティしかない record は、public record Foo(string Name, int Index); と1行で書くことも可能。
つまり下の2つが等価になるはず。
public record class Foo(string Name);
public record class Foo
{
public string Name { get; init; } = "";
public Foo(string name) { Name = name; }
}
ただ引数付きのコンストラクタしかなくなるので、new() { Name = "name" };みたいな書き方は出来なくなる模様。
しかしこの場合もfoo = foo with { Name = "name" };は可能なのでちょいややこしい…
更にrecord classは{ get; init; }だけど、record structは{ get; set; }になるので、更にややこしさが追加。