読み取り専用プロパティの重要性
C#のクラス設計において「カプセル化」は非常に重要な原則です。これは、クラスの内部データをprivateフィールドで保護し、外部からの不正なアクセスや意図しない変更を防ぐことを意味します。
特に、オブジェクトが一度作成されたら変更されたくないデータ(例: ID、作成日時、設定値など)を外部に公開する場合、読み取りは許可するが、書き込みは禁止する「読み取り専用プロパティ」が必要になります。
C#では、この読み取り専用の動作を実現するために、いくつかの異なる構文が提供されています。
1. { get; } (Get-only Auto-Property)
C# 6.0以降で導入された、真の「読み取り専用」自動実装プロパティを定義する最も簡潔な方法です。
setアクセサーを省略することで、このプロパティに値を設定できるのは宣言時のインライン初期化、またはクラスのコンストラクタ内部のみに厳密に制限されます。
一度インスタンスが生成されると、そのクラスの内部メソッドからでさえ、このプロパティの値を変更することはできません。
2. { get; private set; } (Private Setter)
これは、外部のクラスからは読み取り専用に見えますが、クラスの内部からは書き込みが可能なプロパティを定義する方法です。
get;:public(デフォルト)であり、外部から値を読み取れます。private set;:setアクセサーをprivateに設定し、書き込みをクラス内部(コンストラクタや他のメソッド)に限定します。
3. => (Expression-Bodied Property / 算出プロパティ)
これは、データを格納するためのプロパティ(上記1, 2)とは異なります。=>(ラムダ)を使用して定義されるプロパティは、それ自体が値を保持(格納)しません。
代わりに、プロパティがアクセスされるたびに、=> の右側の式が評価(計算)され、その結果が返されます。setアクセサーを持たないため、本質的に読み取り専用です。
コード例:3つのパターンの実装
LogEntry(ログエントリ)クラスを例に、3つの読み取り専用パターンを実装します。
LogID: 生成時に決定され、絶対に変わらないID ({ get; })Severity: 生成時に設定されるが、内部的に変更(例: 格上げ)される可能性があるレベル ({ get; private set; })Summary: 常に他のプロパティから計算される概要 (=>)
using System;
/// <summary>
/// ログエントリを表すクラス
/// </summary>
public class LogEntry
{
// 1. Get-only Auto-Property
// コンストラクタ(またはインライン)でのみ設定可能。
// 生成後は、クラス内部からでも変更不可。
public Guid LogID { get; }
// 2. Private Setter
// 外部からは読み取り専用。
// クラス内部 (コンストラクタやメソッド) からは変更可能。
public string Message { get; private set; }
// 3. Expression-Bodied Property (算出プロパティ)
// データを保持せず、アクセスされるたびに値を計算して返す。
// 常に読み取り専用。
public string Summary => $"[{this.LogID.ToString().Substring(0, 8)}]: {this.Message}";
// コンストラクタ
public LogEntry(string message)
{
// { get; } プロパティはコンストラクタで設定可能
this.LogID = Guid.NewGuid();
// { get; private set; } プロパティもコンストラクタで設定可能
this.Message = message;
}
// クラス内部のメソッド
public void AppendToMessage(string additionalInfo)
{
// { get; private set; } (Message) は内部から変更可能
this.Message += $" // {additionalInfo}";
// { get; } (LogID) は内部からでも変更不可
// this.LogID = Guid.NewGuid(); // コンパイルエラー!
}
}
実行例と動作確認
using System;
public class Program
{
public static void Main()
{
// 1. コンストラクタで初期化
var log = new LogEntry("System boot process started.");
// 2. 読み取り専用プロパティの読み取り
Console.WriteLine($"ID: {log.LogID}");
Console.WriteLine($"Message: {log.Message}");
// 3. 算出プロパティの読み取り
Console.WriteLine($"Summary: {log.Summary}");
Console.WriteLine("---");
// 4. 外部からの書き込みはすべてコンパイルエラー
// log.LogID = Guid.NewGuid(); // エラー: 'get' のみ
// log.Message = "New text"; // エラー: 'set' は private
// log.Summary = "New summary"; // エラー: 読み取り専用
// 5. 内部メソッド経由での 'private set' の変更
log.AppendToMessage("Boot successful.");
Console.WriteLine("--- 内部メソッド実行後 ---");
Console.WriteLine($"Updated Message: {log.Message}");
// Summary (算出プロパティ) も自動的に更新された内容を反映する
Console.WriteLine($"Updated Summary: {log.Summary}");
}
}
出力結果:
ID: 8a1b2c3d-....
Message: System boot process started.
Summary: [8a1b2c3d]: System boot process started.
---
--- 内部メソッド実行後 ---
Updated Message: System boot process started. // Boot successful.
Updated Summary: [8a1b2c3d]: System boot process started. // Boot successful.
まとめ
C#で読み取り専用プロパティを定義する方法は、そのプロパティの「不変性」のレベルによって使い分けます。
public string Prop { get; }: (推奨) オブジェクト生成時(コンストラクタ)にのみ値を設定し、その後は完全に変更不可(不変)にしたい場合。public string Prop { get; private set; }: 外部には読み取り専用として公開しつつ、クラスの内部メソッドでは値を変更する必要がある場合。public string Prop => ...;: データを保持せず、他のプロパティの値などから動的に計算した結果を返したい場合。
