C# readonly propertyとは?読み取り専用プロパティの書き方とget-only・initとの違いを解説
はじめに
C#でクラスやオブジェクトを設計していると、「この値は外部から変更されたくない」「作成時に決まった値を安全に扱いたい」という場面がよくあります。そのようなときに使われるのが、読み取り専用プロパティです。
検索や会話では「C# readonly property」という表現が使われることがありますが、C#ではプロパティそのものに常にreadonly修飾子を付けるわけではありません。多くの場合、getのみを持つプロパティ、initアクセサを持つプロパティ、readonlyフィールドを利用したプロパティなどをまとめて「readonly property」「読み取り専用プロパティ」と呼んでいます。
この記事では、C#におけるreadonly propertyの意味、get-onlyプロパティの書き方、initとの違い、readonlyフィールドやconst、private setとの使い分けまで、実践で迷いやすいポイントを整理して解説します。
1. C#のreadonly propertyとは?
1-1. readonly propertyの意味と「読み取り専用プロパティ」の基本
C#におけるreadonly propertyとは、一般的には「外部から値を変更できないプロパティ」を指します。日本語では「読み取り専用プロパティ」と呼ばれることが多いです。
たとえば、次のようなプロパティは外部から読み取ることはできますが、外部から代入することはできません。
C#public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
}
このNameプロパティはgetのみを持っているため、クラスの外部から次のように値を取得できます。
C#var user = new User("Taro");
Console.WriteLine(user.Name);
一方で、次のような代入はできません。
C#user.Name = "Jiro"; // コンパイルエラー
つまり、読み取り専用プロパティは「値を公開したいが、外部から自由に変更されたくない」場合に使う仕組みです。
1-2. C#における「readonly property」は正式な構文名なのか
厳密にいうと、C#には一般的なクラスの自動実装プロパティに対して、次のようにreadonly修飾子を付ける構文はありません。
C#public readonly string Name { get; } // 通常のクラスではこの書き方はできない
C#でreadonlyというキーワードがよく使われるのは、主にフィールドです。
C#private readonly string _name;
そのため、「readonly property」という言葉は、C#の正式な構文名というよりも、実務上の呼び方として使われることが多い表現です。
C#で読み取り専用の性質を持つプロパティを作る方法には、主に次のようなものがあります。
C#public string Name { get; }
C#public string Name { get; init; }
C#private readonly string _name;
public string Name => _name;
これらはいずれも、外部から安易に値を書き換えられない設計を実現するために使われます。
1-3. 読み取り専用プロパティが使われる場面
読み取り専用プロパティは、オブジェクトの状態を安全に保ちたい場面でよく使われます。
代表的な例は、ID、作成日時、設定値、計算結果、値オブジェクトのプロパティなどです。
C#public class Order
{
public Guid Id { get; }
public DateTime CreatedAt { get; }
public Order()
{
Id = Guid.NewGuid();
CreatedAt = DateTime.UtcNow;
}
}
この例では、注文IDと作成日時はオブジェクト生成時に決まり、その後に外部から変更されるべきではありません。このような値をset可能にしてしまうと、意図しないタイミングで重要なデータが書き換えられる可能性があります。
読み取り専用プロパティを使うことで、「この値は作成後に変更しない」という意図をコード上で明確にできます。
1-4. setできないプロパティと変更できないオブジェクトの違い
注意したいのは、「プロパティにsetがないこと」と「オブジェクトが完全に変更不能であること」は同じではない、という点です。
たとえば次のコードを見てください。
C#public class Team
{
public List<string> Members { get; }
public Team()
{
Members = new List<string>();
}
}
Membersプロパティにはsetがないため、外部から別のリストを代入することはできません。
C#var team = new Team();
// これはできない
team.Members = new List<string>();
しかし、リストの中身を変更することはできます。
C#team.Members.Add("Taro");
team.Members.Add("Jiro");
つまり、読み取り専用プロパティは「プロパティ自体に別の値を代入できない」だけであり、参照先のオブジェクトの中身まで自動的に不変にするわけではありません。
完全に変更できない設計にしたい場合は、IReadOnlyList<T>を使う、コレクションをコピーして返す、不変コレクションを使うなど、追加の工夫が必要です。
2. C#で読み取り専用プロパティを書く基本構文
2-1. getのみを持つget-onlyプロパティ
C#で最も基本的な読み取り専用プロパティは、getのみを持つプロパティです。
C#public class Product
{
public string Name { get; }
public Product(string name)
{
Name = name;
}
}
public string Name { get; }のようにsetを書かないことで、クラス外部からの代入を禁止できます。
この書き方は「get-onlyプロパティ」と呼ばれます。
C#var product = new Product("Keyboard");
Console.WriteLine(product.Name); // 読み取りはできる
product.Name = "Mouse"; // 代入はできない
get-onlyプロパティは、コンストラクタで値を受け取り、その後は変更しない値を表現するのに向いています。
2-2. コンストラクタで値を設定する書き方
読み取り専用プロパティの代表的な使い方は、コンストラクタで値を設定する方法です。
C#public class Customer
{
public int Id { get; }
public string Name { get; }
public Customer(int id, string name)
{
Id = id;
Name = name;
}
}
この設計では、Customerオブジェクトを作るときにIdとNameを必ず渡す必要があります。
C#var customer = new Customer(1, "Yamada");
コンストラクタで必要な値を受け取ることで、未設定の不完全なオブジェクトが作られにくくなります。
特に、ドメインモデルや値オブジェクトのように「生成時点で正しい状態であること」が重要なクラスでは、この書き方がよく使われます。
2-3. 初期値を直接代入する書き方
読み取り専用プロパティには、宣言と同時に初期値を設定することもできます。
C#public class AppInfo
{
public string AppName { get; } = "SampleApp";
public string Version { get; } = "1.0.0";
}
この書き方は、固定の初期値を持つプロパティを作りたい場合に便利です。
また、現在時刻やGUIDのように、インスタンス生成時に値を決めたい場合にも使えます。
C#public class Session
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime CreatedAt { get; } = DateTime.UtcNow;
}
ただし、コンストラクタ引数に応じて値を変えたい場合は、コンストラクタ内で代入するほうが自然です。
2-4. バッキングフィールドを使った読み取り専用プロパティ
自動実装プロパティではなく、バッキングフィールドを使って読み取り専用プロパティを作ることもできます。
C#public class Employee
{
private readonly string _name;
public string Name
{
get { return _name; }
}
public Employee(string name)
{
_name = name;
}
}
より簡潔に書くなら、式形式のプロパティを使えます。
C#public class Employee
{
private readonly string _name;
public string Name => _name;
public Employee(string name)
{
_name = name;
}
}
この方法では、内部の値をreadonlyフィールドとして保持し、外部にはプロパティ経由で公開します。
バッキングフィールドを使う書き方は、値を返す前に加工したい場合や、内部実装を明確に分けたい場合に便利です。
2-5. 読み取り専用プロパティのサンプルコード
次のコードは、読み取り専用プロパティを使ったシンプルな例です。
C#public class Book
{
public string Isbn { get; }
public string Title { get; }
public decimal Price { get; }
public Book(string isbn, string title, decimal price)
{
Isbn = isbn;
Title = title;
Price = price;
}
public string DisplayName => $"{Title} ({Isbn})";
}
使い方は次のとおりです。
C#var book = new Book("978-0000000000", "C#入門", 2800);
Console.WriteLine(book.Isbn);
Console.WriteLine(book.Title);
Console.WriteLine(book.Price);
Console.WriteLine(book.DisplayName);
この例では、Isbn、Title、Priceはコンストラクタで設定され、その後は外部から変更できません。
また、DisplayNameは他のプロパティをもとに計算して値を返す読み取り専用プロパティです。値を保持しているのではなく、アクセスされるたびに計算結果を返します。
3. get-onlyプロパティの特徴と注意点
3-1. get-onlyプロパティとは
get-onlyプロパティとは、getアクセサだけを持ち、setアクセサを持たないプロパティです。
C#public string Name { get; }
外部からは値を読むことはできますが、代入はできません。
C#Console.WriteLine(user.Name); // OK
user.Name = "New Name"; // NG
get-onlyプロパティは、C#で読み取り専用プロパティを作る最も基本的な方法です。
プロパティの値をコンストラクタで設定し、その後は変更させたくない場合に適しています。
3-2. コンストラクタ内では代入できる理由
get-onlyの自動実装プロパティは、クラス外部からは代入できませんが、コンストラクタ内では代入できます。
C#public class User
{
public string Name { get; }
public User(string name)
{
Name = name; // コンストラクタ内なので代入できる
}
}
これは、オブジェクトの初期化時に必要な値を設定できるようにするためです。
読み取り専用プロパティは「一度も代入できないプロパティ」ではありません。正確には、「初期化後に外部から変更できないプロパティ」と考えると理解しやすいです。
そのため、コンストラクタで値を受け取り、オブジェクトの生成時点で状態を確定させる設計と相性が良いです。
3-3. クラス外部から値を変更できない仕組み
C#のプロパティは、getとsetというアクセサによって読み取りと書き込みの可否を制御します。
C#public string Name { get; set; }
このようにsetがある場合は、外部から値を代入できます。
C#user.Name = "Taro";
一方、次のようにsetがない場合、外部から値を書き込む手段がありません。
C#public string Name { get; }
そのため、次の代入はコンパイルエラーになります。
C#user.Name = "Taro";
読み取り専用プロパティは、実行時に代入を拒否するというより、コンパイル時点で不正な代入を防ぐ仕組みです。これにより、意図しない変更を早い段階で検出できます。
3-4. get-onlyプロパティで発生しやすいコンパイルエラー
get-onlyプロパティでよくあるエラーは、コンストラクタ以外の場所で代入しようとするケースです。
C#public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
public void ChangeName(string name)
{
Name = name; // コンパイルエラー
}
}
get-onlyプロパティは、通常のメソッド内で再代入できません。値を後から変更する必要がある場合は、get-onlyではなく、private setや通常のset、あるいは新しいインスタンスを作る設計を検討します。
また、オブジェクト初期化子で値を設定しようとしてエラーになることもあります。
C#var user = new User
{
Name = "Taro" // get-onlyプロパティには設定できない
};
get-onlyプロパティに値を設定したい場合は、基本的にコンストラクタを使います。
C#var user = new User("Taro");
オブジェクト初期化子で設定したい場合は、initアクセサを使う選択肢があります。
3-5. get-onlyプロパティが向いているケース
get-onlyプロパティは、次のようなケースに向いています。
IDのように生成後に変わらない値を表す場合です。
C#public Guid Id { get; }
作成日時のように、オブジェクト生成時に決まる値にも適しています。
C#public DateTime CreatedAt { get; }
また、コンストラクタで必須項目を受け取り、必ず有効な状態でオブジェクトを作りたい場合にも便利です。
C#public class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
get-onlyプロパティは、シンプルで意図が伝わりやすく、イミュータブルな設計の基本になります。
4. initアクセサとの違い
4-1. initとは何か
initは、オブジェクトの初期化時にだけ値を設定できるアクセサです。
C#public class User
{
public string Name { get; init; }
}
initを使うと、オブジェクト初期化子で値を設定できます。
C#var user = new User
{
Name = "Taro"
};
ただし、初期化が終わった後に値を変更することはできません。
C#user.Name = "Jiro"; // コンパイルエラー
initは、読み取り専用に近い性質を保ちながら、オブジェクト初期化子による柔軟な初期化を可能にする仕組みです。
4-2. get-onlyとinitの書き方の違い
get-onlyプロパティは、getのみを書きます。
C#public string Name { get; }
値は主にコンストラクタで設定します。
C#public User(string name)
{
Name = name;
}
一方、initプロパティは、getとinitを書きます。
C#public string Name { get; init; }
値はオブジェクト初期化子で設定できます。
C#var user = new User
{
Name = "Taro"
};
両者の大きな違いは、「オブジェクト初期化子で値を設定できるかどうか」です。
4-3. オブジェクト初期化子で設定できるかの違い
get-onlyプロパティは、基本的にオブジェクト初期化子で設定できません。
C#public class User
{
public string Name { get; }
}
var user = new User
{
Name = "Taro" // コンパイルエラー
};
コンストラクタを用意する必要があります。
C#public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
}
var user = new User("Taro");
一方、initプロパティであれば、オブジェクト初期化子で設定できます。
C#public class User
{
public string Name { get; init; }
}
var user = new User
{
Name = "Taro"
};
この違いにより、initはDTOや設定クラスのように、プロパティ名を明示しながら初期化したい場面で使いやすい構文です。
4-4. initを使うメリット
initを使うメリットは、初期化の柔軟性と読み取り専用に近い安全性を両立できることです。
たとえば、次のようなクラスを考えます。
C#public class SearchCondition
{
public string Keyword { get; init; }
public int Page { get; init; }
public int PageSize { get; init; }
}
このクラスは、オブジェクト初期化子を使ってわかりやすく値を設定できます。
C#var condition = new SearchCondition
{
Keyword = "C# readonly property",
Page = 1,
PageSize = 20
};
初期化後は値を変更できないため、処理の途中で検索条件が書き換わるリスクを減らせます。
また、record型との相性も良く、データを表す型を簡潔に書きたい場合に便利です。
C#public record UserDto
{
public int Id { get; init; }
public string Name { get; init; }
}
4-5. get-onlyとinitの使い分け
get-onlyプロパティとinitは、どちらも「後から変更しにくいプロパティ」を作るために使えますが、向いている場面が少し異なります。
コンストラクタで必須項目を受け取りたい場合は、get-onlyプロパティが適しています。
C#public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
}
プロパティ名を明示しながら柔軟に初期化したい場合は、initが便利です。
C#public class User
{
public string Name { get; init; }
public int Age { get; init; }
}
値オブジェクトやドメインモデルでは、コンストラクタで不正な値を検証したいケースが多いため、get-onlyプロパティがよく使われます。
C#public class UserName
{
public string Value { get; }
public UserName(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("名前は必須です。");
}
Value = value;
}
}
一方、DTOやAPIレスポンス用の型では、initを使うことで記述が簡潔になります。
5. readonlyフィールド・const・private setとの違い
5-1. readonlyフィールドとの違い
readonlyフィールドは、フィールドに対してreadonly修飾子を付ける構文です。
C#private readonly string _name;
readonlyフィールドは、宣言時またはコンストラクタ内でのみ代入できます。
C#public class User
{
private readonly string _name;
public User(string name)
{
_name = name;
}
public string Name => _name;
}
一方、プロパティは外部公開のための仕組みです。
C#public string Name { get; }
readonlyフィールドは内部状態を保持するために使い、プロパティは外部に値を公開するために使う、という違いがあります。
実務では、外部に公開する値はフィールドではなくプロパティにするのが一般的です。内部でreadonlyフィールドを持ち、それを読み取り専用プロパティで公開する設計もよく使われます。
5-2. constとの違い
constは、コンパイル時に値が決まる定数を定義するためのキーワードです。
C#public const double TaxRate = 0.1;
constは暗黙的に静的な値であり、インスタンスごとに異なる値を持つことはできません。
C#public class AppSettings
{
public const int MaxRetryCount = 3;
}
一方、読み取り専用プロパティやreadonlyフィールドは、インスタンスごとに異なる値を持てます。
C#public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
}
constは、円周率や固定の文字列キーなど、コンパイル時に完全に決まっていて将来変更されにくい値に向いています。
一方、実行時に決まる値や、インスタンスごとに異なる値には、get-onlyプロパティやreadonlyフィールドを使います。
5-3. private setとの違い
private setは、外部からは読み取り専用に見えるが、クラス内部からは変更できるプロパティです。
C#public class User
{
public string Name { get; private set; }
public User(string name)
{
Name = name;
}
public void ChangeName(string name)
{
Name = name;
}
}
この場合、クラス外部からの代入はできません。
C#user.Name = "Jiro"; // コンパイルエラー
しかし、クラス内部のメソッドからは変更できます。
C#public void ChangeName(string name)
{
Name = name; // OK
}
get-onlyプロパティは、コンストラクタで設定した後に通常のメソッドから再代入できません。
C#public string Name { get; }
そのため、「内部でも変更しない」ならget-only、「外部には変更させないが内部では変更したい」ならprivate setが適しています。
5-4. プロパティとフィールドを使い分ける基準
C#では、外部に公開する値にはプロパティを使うのが基本です。
C#public string Name { get; }
フィールドを直接公開することもできますが、将来的な変更に弱くなります。
C#public readonly string Name;
フィールドを公開してしまうと、後から値の検証、変換、計算、インターフェース対応などを入れにくくなります。
プロパティで公開しておけば、内部実装を変更しても利用側のコードに影響を与えにくくなります。
C#private readonly string _firstName;
private readonly string _lastName;
public string FullName => $"{_firstName} {_lastName}";
内部で保持するだけならフィールド、外部に公開するならプロパティ、と考えると使い分けやすくなります。
5-5. 比較表で見るreadonly property関連構文の違い
| 構文 | 外部から変更 | 内部から変更 | 主な用途 |
|---|---|---|---|
public string Name { get; } | できない | 基本的にコンストラクタで設定 | 生成後に変えない値 |
public string Name { get; init; } | 初期化時のみ可能 | 初期化時のみ可能 | DTO、設定クラス、record |
public string Name { get; private set; } | できない | できる | 内部状態として変更したい値 |
private readonly string _name; | 公開しない | コンストラクタで設定 | 内部保持用の不変フィールド |
public const string Name = "value"; | できない | できない | コンパイル時定数 |
このように、似たように見える構文でも、変更できるタイミングや用途が異なります。
「外部から変更できないようにしたい」という目的だけでなく、「内部では変更するのか」「初期化時にどのように値を設定したいのか」「インスタンスごとに値が違うのか」を基準に選ぶことが大切です。
6. 実践で使うreadonly propertyの設計例
6-1. DTOや値オブジェクトで使う例
DTOでは、initを使った読み取り専用に近いプロパティがよく使われます。
C#public class UserDto
{
public int Id { get; init; }
public string Name { get; init; }
public string Email { get; init; }
}
オブジェクト初期化子でわかりやすく値を設定できます。
C#var dto = new UserDto
{
Id = 1,
Name = "Taro",
Email = "taro@example.com"
};
一方、値オブジェクトでは、コンストラクタで検証を行い、get-onlyプロパティで値を公開する設計がよく使われます。
C#public class EmailAddress
{
public string Value { get; }
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("メールアドレスは必須です。");
}
if (!value.Contains("@"))
{
throw new ArgumentException("メールアドレスの形式が不正です。");
}
Value = value;
}
}
この設計により、不正な値を持つEmailAddressオブジェクトが作られにくくなります。
6-2. 設定値やIDを読み取り専用にする例
設定値やIDは、一度決まった後に変わるべきではないことが多いです。
C#public class ApiClientSettings
{
public string BaseUrl { get; }
public int TimeoutSeconds { get; }
public ApiClientSettings(string baseUrl, int timeoutSeconds)
{
BaseUrl = baseUrl;
TimeoutSeconds = timeoutSeconds;
}
}
このようにしておくと、処理の途中で設定値が書き換わることを防げます。
IDも読み取り専用プロパティに向いています。
C#public class Customer
{
public Guid Id { get; }
public string Name { get; private set; }
public Customer(string name)
{
Id = Guid.NewGuid();
Name = name;
}
}
この例では、Idは作成後に変更しない値としてget-onlyにし、Nameは必要に応じて内部で変更できるようにprivate setにしています。
すべてのプロパティを同じルールで設計するのではなく、値の意味に応じて変更可否を決めることが重要です。
6-3. 計算結果を返す読み取り専用プロパティ
読み取り専用プロパティは、保存された値を返すだけでなく、計算結果を返すためにも使えます。
C#public class OrderLine
{
public decimal UnitPrice { get; }
public int Quantity { get; }
public decimal TotalPrice => UnitPrice * Quantity;
public OrderLine(decimal unitPrice, int quantity)
{
UnitPrice = unitPrice;
Quantity = quantity;
}
}
TotalPriceにはsetがありません。なぜなら、合計金額は直接設定する値ではなく、単価と数量から計算される値だからです。
C#var line = new OrderLine(1000, 3);
Console.WriteLine(line.TotalPrice); // 3000
このようなプロパティにsetを用意してしまうと、UnitPriceとQuantityから計算される値と矛盾する可能性があります。
計算で求められる値は、読み取り専用プロパティとして表現すると整合性を保ちやすくなります。
6-4. record型と読み取り専用プロパティ
C#のrecord型は、データを表す型を簡潔に定義するために便利です。
C#public record UserRecord(int Id, string Name);
このように定義すると、IdやNameを持つrecordを簡単に作成できます。
C#var user = new UserRecord(1, "Taro");
recordは、値の等価性やwith式によるコピーと相性が良く、読み取り専用に近いデータ構造を作りたい場合に便利です。
C#var user1 = new UserRecord(1, "Taro");
var user2 = user1 with { Name = "Jiro" };
この例では、user1を直接変更するのではなく、一部の値を変えた新しいuser2を作っています。
読み取り専用プロパティとrecordを組み合わせると、変更による副作用を減らした設計にしやすくなります。
6-5. イミュータブルなクラス設計での活用
イミュータブルなクラスとは、作成後に状態が変わらないクラスのことです。
読み取り専用プロパティは、イミュータブルなクラス設計の基本になります。
C#public class Address
{
public string PostalCode { get; }
public string Prefecture { get; }
public string City { get; }
public Address(string postalCode, string prefecture, string city)
{
PostalCode = postalCode;
Prefecture = prefecture;
City = city;
}
}
このクラスでは、すべてのプロパティがget-onlyであり、コンストラクタでのみ設定されます。
ただし、イミュータブルにしたい場合は、プロパティの型にも注意が必要です。stringやintのように値を変更できない型なら扱いやすいですが、List<T>や配列のような変更可能な型を公開すると、中身を変更される可能性があります。
C#public class Group
{
public IReadOnlyList<string> Members { get; }
public Group(IEnumerable<string> members)
{
Members = members.ToList().AsReadOnly();
}
}
このように、外部に公開する型をIReadOnlyList<T>にするなど、コレクションの扱いにも注意することで、より安全な設計になります。
7. readonly propertyを使うときの注意点
7-1. 参照型プロパティは中身まで不変になるわけではない
読み取り専用プロパティで特に注意したいのが、参照型の扱いです。
C#public class User
{
public Profile Profile { get; }
public User(Profile profile)
{
Profile = profile;
}
}
Profileプロパティにsetがないため、別のProfileオブジェクトを代入することはできません。
C#user.Profile = new Profile(); // コンパイルエラー
しかし、Profileオブジェクトの中に変更可能なプロパティがあれば、その中身は変更できます。
C#user.Profile.DisplayName = "New Name";
つまり、読み取り専用なのはProfileという参照そのものであり、参照先のオブジェクト全体ではありません。
完全に変更を防ぎたい場合は、参照先の型もイミュータブルに設計する必要があります。
7-2. Listや配列を公開すると変更される可能性がある
List<T>や配列を読み取り専用プロパティとして公開しても、中身は変更される可能性があります。
C#public class Cart
{
public List<string> Items { get; } = new List<string>();
}
この場合、外部からItemsに別のリストを代入することはできません。
C#cart.Items = new List<string>(); // コンパイルエラー
しかし、要素の追加や削除はできます。
C#cart.Items.Add("Item1");
cart.Items.Remove("Item1");
配列も同様です。
C#public class Scores
{
public int[] Values { get; }
public Scores(int[] values)
{
Values = values;
}
}
外部から次のように中身を変更できます。
C#scores.Values[0] = 100;
コレクションを安全に公開したい場合は、次のような方法を検討します。
C#public class Cart
{
private readonly List<string> _items = new();
public IReadOnlyList<string> Items => _items;
public void AddItem(string item)
{
_items.Add(item);
}
}
このように、内部ではList<T>を使い、外部にはIReadOnlyList<T>として公開すると、外部から直接変更されにくくなります。
7-3. setをなくせば常に安全とは限らない
setをなくすことは安全な設計の第一歩ですが、それだけで常に十分とは限りません。
たとえば、次のコードではSettingsプロパティ自体は読み取り専用です。
C#public class Application
{
public AppSettings Settings { get; }
public Application(AppSettings settings)
{
Settings = settings;
}
}
しかし、AppSettingsの中身が変更可能であれば、外部から設定値を書き換えられます。
C#application.Settings.TimeoutSeconds = 60;
また、コンストラクタで受け取ったコレクションをそのまま保持すると、呼び出し元が後から元のコレクションを変更できる場合があります。
C#public class Group
{
public IReadOnlyList<string> Members { get; }
public Group(List<string> members)
{
Members = members;
}
}
この場合、呼び出し元がmembersを保持していれば、後から中身を変更できます。
C#var list = new List<string> { "Taro" };
var group = new Group(list);
list.Add("Jiro"); // group.Membersにも影響する
安全性を高めたい場合は、コンストラクタ内でコピーを作るのが有効です。
C#public Group(IEnumerable<string> members)
{
Members = members.ToList().AsReadOnly();
}
7-4. シリアライズやORMで問題になるケース
読み取り専用プロパティは便利ですが、シリアライズライブラリやORMを使う場合には注意が必要です。
たとえば、JSONからオブジェクトを復元する場合、ライブラリがプロパティに値を設定できる必要があります。get-onlyプロパティだけのクラスでは、コンストラクタの形やライブラリの対応状況によって、正しくデシリアライズできないことがあります。
C#public class User
{
public int Id { get; }
public string Name { get; }
public User(int id, string name)
{
Id = id;
Name = name;
}
}
このようなクラスを扱う場合、使用するシリアライザがコンストラクタを使った復元に対応しているか確認する必要があります。
ORMでも同様に、エンティティを復元するために引数なしコンストラクタやprivate setが必要になることがあります。
C#public class User
{
public int Id { get; private set; }
public string Name { get; private set; }
private User()
{
}
public User(string name)
{
Name = name;
}
}
設計上はget-onlyが理想でも、利用するフレームワークとの相性によってはprivate setやinitを選ぶほうが扱いやすい場合があります。
7-5. 可読性と保守性を意識した設計ポイント
読み取り専用プロパティを使うときは、「何を変更不可にしたいのか」を明確にすることが大切です。
何でもget-onlyにすればよいわけではありません。後から状態が変わることが自然な値まで読み取り専用にしてしまうと、かえって使いにくいクラスになります。
たとえば、ユーザー名を変更できる仕様であれば、Nameをget-onlyにするより、変更用メソッドを用意するほうが自然です。
C#public class User
{
public string Name { get; private set; }
public User(string name)
{
Name = name;
}
public void ChangeName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です。");
}
Name = name;
}
}
一方、IDや作成日時のように変更されると困る値はget-onlyにするのが適しています。
C#public Guid Id { get; }
public DateTime CreatedAt { get; }
読み取り専用プロパティは、単に代入を禁止するためのものではなく、クラスの意図を表現するための設計手段です。値の意味を考えて、必要なところに適切に使うことが保守性の高いコードにつながります。
8. readonly propertyに関するよくある質問
8-1. プロパティにreadonly修飾子は付けられる?
通常のクラスのプロパティに対して、次のようにreadonly修飾子を付けることはできません。
C#public readonly string Name { get; }
C#でreadonly修飾子をよく使うのはフィールドです。
C#private readonly string _name;
読み取り専用プロパティを作りたい場合は、readonly修飾子を付けるのではなく、setを書かないget-onlyプロパティにします。
C#public string Name { get; }
または、初期化時のみ設定できるinitを使います。
C#public string Name { get; init; }
そのため、「C# readonly property」と検索されることはありますが、実際のコードではget-onlyプロパティやinitプロパティとして実装するのが一般的です。
8-2. get-onlyプロパティとreadonlyフィールドはどちらを使うべき?
外部に公開する値であれば、基本的にはget-onlyプロパティを使います。
C#public string Name { get; }
クラス内部でだけ使う値であれば、readonlyフィールドを使います。
C#private readonly string _name;
内部ではreadonlyフィールドとして保持し、外部にはプロパティで公開する書き方もあります。
C#public class User
{
private readonly string _name;
public string Name => _name;
public User(string name)
{
_name = name;
}
}
ただし、単純に値を保持するだけなら、自動実装のget-onlyプロパティで十分なことが多いです。
C#public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
}
フィールドを使うかプロパティを使うかは、公開範囲と内部実装の複雑さで判断するとよいでしょう。
8-3. initはどのバージョンのC#から使える?
initアクセサはC# 9.0から使える機能です。
C#public string Name { get; init; }
C# 9.0以降を使える環境であれば、オブジェクト初期化子と組み合わせて、初期化時だけ値を設定できるプロパティを作れます。
C#var user = new User
{
Name = "Taro"
};
古いバージョンのC#ではinitが使えないため、get-onlyプロパティとコンストラクタを使う方法が基本になります。
C#public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
}
プロジェクトで使えるC#のバージョンによって、initを使うかどうかを判断しましょう。
8-4. 読み取り専用プロパティの値を後から変更したい場合は?
読み取り専用プロパティの値を後から変更したい場合、まず「本当に変更可能にすべき値なのか」を考える必要があります。
後から変更する必要があるなら、private setを使って外部からの直接変更だけを防ぐ方法があります。
C#public class User
{
public string Name { get; private set; }
public User(string name)
{
Name = name;
}
public void ChangeName(string name)
{
Name = name;
}
}
この場合、外部から直接代入することはできませんが、クラスが用意したメソッドを通じて変更できます。
C#user.ChangeName("Jiro");
一方、イミュータブルな設計を維持したい場合は、既存のオブジェクトを変更するのではなく、新しいオブジェクトを作る方法もあります。
C#public record User(int Id, string Name);
var user1 = new User(1, "Taro");
var user2 = user1 with { Name = "Jiro" };
変更可能にするのか、新しいインスタンスを作るのかは、クラスの責務や設計方針に合わせて決めます。
8-5. 完全に変更できないクラスを作るにはどうすればよい?
完全に変更できないクラスを作るには、単にプロパティをget-onlyにするだけでは不十分な場合があります。
基本的には、次のような点を意識します。
すべてのプロパティをget-onlyまたはinitにします。
C#public string Name { get; }
コンストラクタで必要な値をすべて受け取り、検証します。
C#public User(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です。");
}
Name = name;
}
変更可能なコレクションをそのまま公開しないようにします。
C#private readonly List<string> _roles;
public IReadOnlyList<string> Roles => _roles;
コンストラクタで受け取ったコレクションはコピーします。
C#public User(IEnumerable<string> roles)
{
_roles = roles.ToList();
}
参照型のプロパティを持つ場合は、その参照先の型も変更不能に近い設計にします。
C#public class Profile
{
public string DisplayName { get; }
public Profile(string displayName)
{
DisplayName = displayName;
}
}
このように、プロパティ、フィールド、コンストラクタ、コレクション、参照型の設計を組み合わせることで、より安全なイミュータブルクラスを作ることができます。
まとめ
C#のreadonly propertyとは、一般的には外部から値を変更できない読み取り専用プロパティを指す言葉です。ただし、C#の通常のプロパティにreadonly修飾子を付けるわけではなく、主にget-onlyプロパティ、initプロパティ、readonlyフィールドを使って読み取り専用の設計を実現します。
get-onlyプロパティは、public string Name { get; }のようにsetを書かないプロパティです。コンストラクタで値を設定し、その後は外部から変更できないため、ID、作成日時、値オブジェクトなどに向いています。
initは、C# 9.0から使える初期化専用のアクセサです。オブジェクト初期化子で値を設定でき、初期化後は変更できません。DTOや設定クラス、record型と相性が良い構文です。
readonlyフィールドは内部状態を不変に保つために使い、constはコンパイル時に決まる定数に使います。private setは、外部からは変更できないがクラス内部では変更したい場合に便利です。
ただし、読み取り専用プロパティにしただけでオブジェクト全体が完全に不変になるわけではありません。特に、参照型、List<T>、配列を扱う場合は、中身が変更される可能性に注意が必要です。
C#でreadonly propertyを使うときは、「外部から変更させたくない値なのか」「内部では変更する必要があるのか」「初期化時にどのように値を設定したいのか」を考えて、get-only、init、private set、readonlyフィールドを適切に使い分けることが大切です。

