C#のreadonlyとは?constとの違い・使い方・注意点を初心者向けに徹底解説

はじめに

C#でクラスやフィールドを設計していると、readonlyというキーワードを見かけることがあります。

readonlyは、簡単にいうと「初期化したあとは再代入できない」ことを表すためのキーワードです。たとえば、作成日時、ID、設定値、依存オブジェクトなど、プログラムの途中で勝手に変わってほしくない値を守るために使います。

C#には似たようなキーワードとしてconstもあります。どちらも「変更できない値」を扱うために使われますが、使える場面や初期化のタイミング、扱える型には大きな違いがあります。

この記事では、C#のreadonlyについて、初心者にもわかりやすいように、基本的な意味、使い方、constとの違い、static readonlyとの使い分け、注意点まで順番に解説します。

1. C#のreadonlyとは?初心者向けに基本をわかりやすく解説

1-1. readonlyは「初期化後に再代入できない」ことを表すキーワード

C#のreadonlyは、フィールドに対して使うことで「一度値を設定したら、そのあと別の値を代入できない」ようにするキーワードです。

たとえば、次のように書きます。

C#
public class User
{
private readonly int id;

public User(int id)
{
this.id = id;
}
}

この例では、idフィールドにreadonlyが付いています。

idはコンストラクターの中で一度だけ値を設定できますが、その後は別の値を代入できません。

C#
public class User
{
private readonly int id;

public User(int id)
{
this.id = id; // OK
}

public void ChangeId(int newId)
{
// this.id = newId; // エラー
}
}

つまり、readonlyは「この値は初期化後に変えない」という設計上の意図をコードで表すために使います。

1-2. readonlyが使える主な場面

C#のreadonlyは、主に次のような場面で使われます。

C#
private readonly int id;
private readonly string name;
private readonly DateTime createdAt;
private readonly ILogger logger;

代表的なのは、クラスのフィールドです。

特に次のような値に使われることが多いです。

用途
IDユーザーID、商品ID、注文ID
作成日時オブジェクト生成時の日時
設定値タイムアウト秒数、最大件数
依存オブジェクトロガー、リポジトリ、サービス
実行時に決まる値DateTime.NowGuid.NewGuid()

たとえば、作成日時を保持する場合は次のように書けます。

C#
public class Order
{
private readonly DateTime createdAt;

public Order()
{
createdAt = DateTime.Now;
}
}

createdAtは注文オブジェクトが作られた時点の日時を表すため、あとから変更されると困ります。このような値にはreadonlyが向いています。

1-3. readonlyを使うと何が嬉しいのか

readonlyを使うメリットは、単に「代入できなくなる」だけではありません。

一番大きなメリットは、コードの意図が明確になることです。

C#
private readonly string connectionString;

このように書かれていると、他の開発者は「このconnectionStringは初期化後に変わらない値なんだな」とすぐに理解できます。

また、誤って値を書き換えるミスも防げます。

C#
public class Product
{
private readonly int productId;

public Product(int productId)
{
this.productId = productId;
}

public void Update()
{
// productId = 999; // コンパイルエラーになるため、誤代入を防げる
}
}

コンパイル時にエラーとして検出されるため、実行してからバグに気づくのではなく、コードを書いている段階で問題を発見できます。

1-4. readonlyと「変更できない値」の関係を整理

readonlyは「変更できない値」を扱うためのキーワードですが、正確には「フィールドへの再代入を禁止する」ものです。

ここはとても重要です。

C#
private readonly List<string> names = new List<string>();

この場合、namesに別のList<string>を代入することはできません。

C#
// names = new List<string>(); // エラー

しかし、リストの中身を変更することはできます。

C#
names.Add("Alice"); // OK

つまり、readonlyは「参照そのものを変えられない」だけで、「参照先のオブジェクトの中身まで完全に変更不可にする」わけではありません。

この点は、readonlyを理解するうえで非常に重要です。

2. readonlyフィールドの基本的な使い方

2-1. フィールド宣言時にreadonlyを付ける書き方

readonlyは、フィールド宣言に付けて使います。

基本形は次のとおりです。

C#
アクセス修飾子 readonly 型名 フィールド名;

たとえば、int型の読み取り専用フィールドを定義するなら次のようになります。

C#
private readonly int number;

文字列の場合は次のように書きます。

C#
private readonly string name;

クラス型にも使えます。

C#
private readonly Customer customer;

実際のクラスでは、次のように使います。

C#
public class User
{
private readonly int id;
private readonly string name;

public User(int id, string name)
{
this.id = id;
this.name = name;
}
}

idnameはコンストラクターで初期化されたあと、変更できません。

2-2. 宣言時に初期化する方法

readonlyフィールドは、宣言と同時に初期化できます。

C#
public class Sample
{
private readonly int maxCount = 100;
}

この場合、maxCountには最初から100が設定されます。

文字列でも同じです。

C#
public class AppInfo
{
private readonly string appName = "SampleApp";
}

DateTimeGuidのように、実行時に値が決まるものも設定できます。

C#
public class RequestInfo
{
private readonly DateTime receivedAt = DateTime.Now;
private readonly Guid requestId = Guid.NewGuid();
}

これはconstではできません。

DateTime.NowGuid.NewGuid()は実行時に評価される値だからです。

2-3. コンストラクター内で初期化する方法

readonlyフィールドは、コンストラクター内でも初期化できます。

C#
public class User
{
private readonly int id;
private readonly string name;

public User(int id, string name)
{
this.id = id;
this.name = name;
}
}

この書き方は非常によく使われます。

外部から渡された値を、オブジェクトの生成時に固定したい場合に便利です。

C#
var user = new User(1, "Alice");

この時点でidnameが設定され、その後は再代入できなくなります。

2-4. コンストラクターごとに異なる値を設定する方法

readonlyフィールドは、コンストラクターごとに異なる値を設定できます。

C#
public class User
{
private readonly string role;

public User()
{
role = "Guest";
}

public User(string role)
{
this.role = role;
}
}

この例では、引数なしコンストラクターを使うとrole"Guest"になります。

C#
var user1 = new User();

一方、引数ありコンストラクターを使うと、指定した値になります。

C#
var user2 = new User("Admin");

readonlyは「すべてのインスタンスで同じ値にする」という意味ではありません。

インスタンスを作るときに値を決め、そのインスタンス内では変更できない、という意味です。

2-5. コンストラクター外で代入しようとした場合のエラー例

readonlyフィールドは、宣言時またはコンストラクター内でしか代入できません。

次のコードはエラーになります。

C#
public class User
{
private readonly int id;

public User(int id)
{
this.id = id; // OK
}

public void ChangeId(int newId)
{
this.id = newId; // エラー
}
}

ChangeIdメソッドの中でidに再代入しようとしているため、コンパイルエラーになります。

正しくは、readonlyにしたい値はコンストラクターで決めるようにします。

C#
public class User
{
private readonly int id;

public User(int id)
{
this.id = id;
}

public int GetId()
{
return id;
}
}

もしあとから変更する必要がある値なら、readonlyを付けるべきではありません。

C#
public class User
{
private int age;

public void ChangeAge(int newAge)
{
age = newAge; // OK
}
}

「変更されてよい値」には通常のフィールドやプロパティを使い、「変更されたくない値」にはreadonlyを使う、という考え方が基本です。

3. readonlyとconstの違い

3-1. constはコンパイル時定数、readonlyは実行時定数

C#のreadonlyとよく比較されるのがconstです。

どちらも「変更できない値」を表すために使われますが、決定的な違いがあります。

constはコンパイル時に値が決まる定数です。

C#
public const int MaxCount = 100;

この100は、プログラムをコンパイルする時点で決まっています。

一方、readonlyは実行時に値を決めることができます。

C#
public readonly DateTime CreatedAt = DateTime.Now;

DateTime.Nowはプログラムを実行した瞬間に決まる値です。そのため、constにはできませんが、readonlyにはできます。

C#
// public const DateTime CreatedAt = DateTime.Now; // エラー

public readonly DateTime CreatedAt = DateTime.Now; // OK

この違いは非常に重要です。

3-2. 初期化できるタイミングの違い

constは、宣言時に必ず値を設定する必要があります。

C#
public const int MaxCount = 100;

次のように、あとからコンストラクターで設定することはできません。

C#
public class Sample
{
// public const int MaxCount; // エラー

public Sample()
{
// MaxCount = 100; // エラー
}
}

一方、readonlyは宣言時またはコンストラクター内で初期化できます。

C#
public class Sample
{
public readonly int MaxCount;

public Sample()
{
MaxCount = 100;
}
}

また、コンストラクターの引数によって値を変えることもできます。

C#
public class Sample
{
public readonly int MaxCount;

public Sample(int maxCount)
{
MaxCount = maxCount;
}
}

この柔軟さがreadonlyの大きな特徴です。

3-3. 扱える型の違い

constで扱える型は限られています。

主に次のような型です。

constで使えるか
int使える
double使える
bool使える
char使える
string使える
enum使える
DateTime使えない
Guid使えない
任意のクラス使えない

たとえば、これは使えます。

C#
public const int MaxCount = 100;
public const string AppName = "SampleApp";

しかし、これは使えません。

C#
// public const DateTime StartedAt = DateTime.Now; // エラー
// public const Guid Id = Guid.NewGuid(); // エラー

readonlyであれば、DateTimeGuidも使えます。

C#
public readonly DateTime StartedAt = DateTime.Now;
public readonly Guid Id = Guid.NewGuid();

また、自作クラスのインスタンスもreadonlyフィールドとして保持できます。

C#
public class UserService
{
private readonly UserRepository repository;

public UserService(UserRepository repository)
{
this.repository = repository;
}
}

3-4. staticが自動で付くかどうかの違い

constは暗黙的にstaticです。

つまり、次のようなconstフィールドは、インスタンスごとではなくクラスに属する値です。

C#
public class AppSettings
{
public const int MaxCount = 100;
}

使うときは、クラス名からアクセスできます。

C#
int count = AppSettings.MaxCount;

constに対してstaticを明示的に付けることはできません。

C#
// public static const int MaxCount = 100; // エラー

一方、readonlyは通常、インスタンスごとのフィールドです。

C#
public class User
{
public readonly int Id;

public User(int id)
{
Id = id;
}
}

この場合、Userオブジェクトごとに異なるIdを持てます。

C#
var user1 = new User(1);
var user2 = new User(2);

クラス全体で1つだけ共有したい場合は、static readonlyを使います。

C#
public class AppSettings
{
public static readonly int MaxCount = 100;
}

3-5. switch文や属性引数で使えるかの違い

constはコンパイル時定数なので、switch文のcaseラベルなどに使えます。

C#
public class Status
{
public const int Active = 1;
public const int Inactive = 2;
}

int status = 1;

switch (status)
{
case Status.Active:
Console.WriteLine("有効");
break;

case Status.Inactive:
Console.WriteLine("無効");
break;
}

一方、readonlyは実行時に値が決まる可能性があるため、caseラベルには使えません。

C#
public class Status
{
public static readonly int Active = 1;
}

// switch (status)
// {
// case Status.Active: // エラー
// break;
// }

また、属性の引数にも基本的にはコンパイル時定数が必要です。

C#
public const string CategoryName = "Sample";

[Obsolete(CategoryName)]
public void OldMethod()
{
}

readonlyフィールドは属性引数として使えません。

C#
public static readonly string CategoryName = "Sample";

// [Obsolete(CategoryName)] // エラー
public void OldMethod()
{
}

switchcaseや属性引数で使いたい値は、constを選ぶ必要があります。

3-6. アセンブリ変更時の影響の違い

conststatic readonlyの違いで、実務上とても重要なのがアセンブリ変更時の影響です。

public constの値は、利用側のプログラムに埋め込まれることがあります。

たとえば、ライブラリ側に次のコードがあるとします。

C#
public class Settings
{
public const int MaxCount = 100;
}

別のプロジェクトでこれを使います。

C#
int count = Settings.MaxCount;

このとき、利用側のコードには100という値がコンパイル時に埋め込まれる場合があります。

その後、ライブラリ側で値を変更したとします。

C#
public class Settings
{
public const int MaxCount = 200;
}

ライブラリだけ差し替えても、利用側を再コンパイルしないと古い値100が使われ続ける可能性があります。

一方、static readonlyは実行時に値を参照します。

C#
public class Settings
{
public static readonly int MaxCount = 200;
}

そのため、外部に公開する固定値で、将来値を変更する可能性がある場合は、constよりstatic readonlyのほうが安全なことがあります。

3-7. readonlyとconstの違いを比較表で整理

readonlyconstの違いを表で整理すると、次のようになります。

比較項目constreadonly
値が決まるタイミングコンパイル時実行時でも可能
初期化できる場所宣言時のみ宣言時、コンストラクター内
扱える型限定的ほぼ任意の型
DateTime.Now使えない使える
Guid.NewGuid()使えない使える
暗黙的にstaticかはいいいえ
インスタンスごとに値を持てるかできないできる
switch caseで使えるか使える使えない
属性引数で使えるか使える使えない
外部公開時の値変更利用側の再コンパイルが必要になる場合がある実行時に参照される

単純な数値や文字列で、絶対に変わらない値ならconstが向いています。

一方、実行時に決まる値や、将来変更される可能性のある公開値にはreadonlystatic readonlyが向いています。

4. readonlyとstatic readonlyの違い

4-1. readonlyはインスタンスごとに値を持てる

通常のreadonlyフィールドは、インスタンスごとに値を持ちます。

C#
public class User
{
public readonly int Id;

public User(int id)
{
Id = id;
}
}

この場合、次のようにユーザーごとに異なるIdを持てます。

C#
var user1 = new User(1);
var user2 = new User(2);

Console.WriteLine(user1.Id); // 1
Console.WriteLine(user2.Id); // 2

readonlyは「全員で同じ値を持つ」という意味ではありません。

「それぞれのインスタンスで、初期化後に値を変えられない」という意味です。

そのため、ユーザーID、注文ID、作成日時など、オブジェクトごとに異なるが途中で変わってほしくない値に適しています。

4-2. static readonlyはクラス全体で1つの値を共有する

static readonlyは、クラス全体で1つだけ値を持ちます。

C#
public class AppSettings
{
public static readonly int MaxRetryCount = 3;
}

使うときは、インスタンスを作らずにクラス名からアクセスします。

C#
Console.WriteLine(AppSettings.MaxRetryCount);

static readonlyは、すべてのインスタンスで共有される値です。

たとえば、アプリケーション全体で共通の設定値を持つ場合に使えます。

C#
public class FileSettings
{
public static readonly string DefaultExtension = ".txt";
}

4-3. static readonlyが向いているケース

static readonlyは、次のようなケースに向いています。

ケース
クラス全体で共有する設定値最大リトライ回数、標準タイムアウト
実行時に決まる固定値起動日時、環境変数から取得した値
constにできない型DateTimeGuid、配列、オブジェクト
将来変更される可能性がある公開値ライブラリの公開設定値

たとえば、アプリケーションの起動時刻を保持する場合は次のように書けます。

C#
public class AppInfo
{
public static readonly DateTime StartedAt = DateTime.Now;
}

DateTime.Nowは実行時に決まる値なので、constにはできません。

C#
// public const DateTime StartedAt = DateTime.Now; // エラー

このような場合はstatic readonlyが適しています。

4-4. constではなくstatic readonlyを選ぶべきケース

次のような場合は、constではなくstatic readonlyを選ぶことを検討しましょう。

まず、値が実行時に決まる場合です。

C#
public static readonly Guid ApplicationId = Guid.NewGuid();

Guid.NewGuid()は実行時に実行されるため、constにはできません。

次に、DateTimeのようにconstで扱えない型を使う場合です。

C#
public static readonly DateTime ReleaseDate = new DateTime(2026, 1, 1);

また、外部に公開する値で、将来変更する可能性がある場合もstatic readonlyが向いています。

C#
public class ApiSettings
{
public static readonly string BaseUrl = "https://api.example.com";
}

public constにすると、利用側のプログラムに値が埋め込まれる可能性があります。将来値を変える可能性があるなら、static readonlyのほうが安全です。

4-5. readonly・static readonly・constの使い分け早見表

readonlystatic readonlyconstの使い分けは、次のように考えるとわかりやすいです。

使いたい値選ぶもの理由
インスタンスごとに異なるIDreadonlyオブジェクトごとに値を持てる
作成日時readonly実行時に決まる
クラス全体で共有する設定値static readonly全体で1つだけ持てる
DateTime.Nowの値readonlyまたはstatic readonlyconstでは使えない
絶対に変わらない数値constコンパイル時定数として扱える
switch caseで使う値constコンパイル時定数が必要
外部公開する変更可能性のある固定値static readonly利用側への埋め込みを避けやすい

迷った場合は、まず次のように考えると判断しやすくなります。

コンパイル時に完全に決まる、絶対に変わらない値 → const
実行時に決まる、または型に制限を受けたくない値 → readonly
クラス全体で共有したい実行時定数 → static readonly

5. readonlyの具体的なコード例

5-1. IDや作成日時をreadonlyで保持する例

readonlyの代表的な使い方は、IDや作成日時の保持です。

C#
public class Order
{
private readonly int orderId;
private readonly DateTime createdAt;

public Order(int orderId)
{
this.orderId = orderId;
this.createdAt = DateTime.Now;
}

public void Print()
{
Console.WriteLine($"注文ID: {orderId}");
Console.WriteLine($"作成日時: {createdAt}");
}
}

このコードでは、orderIdcreatedAtはコンストラクターで設定されたあと変更されません。

注文IDや作成日時は、あとから勝手に変わるとデータの整合性が崩れます。

そのため、readonlyを付けて「変更しない値」として扱うのが自然です。

プロパティとして外部に公開したい場合は、次のようにできます。

C#
public class Order
{
private readonly int orderId;
private readonly DateTime createdAt;

public Order(int orderId)
{
this.orderId = orderId;
this.createdAt = DateTime.Now;
}

public int OrderId => orderId;
public DateTime CreatedAt => createdAt;
}

外部から値を読み取ることはできますが、直接書き換えることはできません。

5-2. 設定値をstatic readonlyで定義する例

アプリケーション全体で共有する設定値には、static readonlyが使えます。

C#
public class AppSettings
{
public static readonly int MaxRetryCount = 3;
public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
}

使うときは、クラス名からアクセスします。

C#
Console.WriteLine(AppSettings.MaxRetryCount);
Console.WriteLine(AppSettings.Timeout);

TimeSpan.FromSeconds(30)のようなメソッド呼び出しは、constでは使えません。

C#
// public const TimeSpan Timeout = TimeSpan.FromSeconds(30); // エラー

このように、実行時に生成される値や、constで扱えない型にはstatic readonlyが便利です。

5-3. DateTimeやGuidなど実行時に決まる値を扱う例

DateTime.NowGuid.NewGuid()は、実行時に値が決まります。

そのため、readonlyと相性がよいです。

C#
public class RequestContext
{
public readonly Guid RequestId;
public readonly DateTime CreatedAt;

public RequestContext()
{
RequestId = Guid.NewGuid();
CreatedAt = DateTime.Now;
}
}

この例では、リクエストごとに一意のIDと作成日時を持たせています。

C#
var request = new RequestContext();

Console.WriteLine(request.RequestId);
Console.WriteLine(request.CreatedAt);

一度作成されたリクエストのIDや作成日時は変更されるべきではないため、readonlyが適しています。

5-4. クラスや構造体でreadonlyを使う例

readonlyはフィールドだけでなく、構造体の設計でも重要です。

たとえば、座標を表す構造体を考えてみます。

C#
public readonly struct Point
{
public double X { get; }
public double Y { get; }

public Point(double x, double y)
{
X = x;
Y = y;
}
}

readonly structにすると、その構造体が変更されない値として扱われることを表せます。

C#
var point = new Point(10, 20);

Console.WriteLine(point.X);
Console.WriteLine(point.Y);

また、クラスの中で依存オブジェクトをreadonlyにする例もよく使われます。

C#
public class UserService
{
private readonly UserRepository repository;

public UserService(UserRepository repository)
{
this.repository = repository;
}

public void Register(string name)
{
repository.Save(name);
}
}

この場合、repositoryはコンストラクターで受け取ったあと変更されません。

依存オブジェクトが途中で別のものに入れ替わると、クラスの動作がわかりにくくなります。readonlyを付けることで、設計が安定します。

5-5. よくあるエラーコードと正しい修正例

readonlyでよくある間違いは、コンストラクター外で代入しようとすることです。

C#
public class Sample
{
private readonly int value;

public Sample()
{
value = 10; // OK
}

public void SetValue(int newValue)
{
value = newValue; // エラー
}
}

この場合、valuereadonlyなので、SetValueメソッド内では代入できません。

修正方法は、値をコンストラクターで渡すことです。

C#
public class Sample
{
private readonly int value;

public Sample(int value)
{
this.value = value;
}
}

また、値をあとから変更したいなら、readonlyを付けない設計にします。

C#
public class Sample
{
private int value;

public void SetValue(int newValue)
{
value = newValue; // OK
}
}

readonlyは「変更しない値」にだけ使うべきです。

「変更したい値」に無理に付けると、設計とコードが矛盾してしまいます。

6. readonlyを使うときの注意点

6-1. readonlyは「参照先の中身」までは不変にしない

readonlyを使うと、フィールドへの再代入はできなくなります。

しかし、参照型の場合は「参照先の中身」までは不変になりません。

次のコードを見てください。

C#
public class Sample
{
private readonly List<string> names = new List<string>();

public void AddName(string name)
{
names.Add(name); // OK
}
}

namesにはreadonlyが付いています。

そのため、別のリストを代入することはできません。

C#
// names = new List<string>(); // エラー

しかし、names.Add(name)のように、リストの中身を変更することはできます。

これは、readonlyが守っているのは「フィールドが参照している先を変えないこと」だからです。

6-2. 配列やListをreadonlyにしても要素は変更できる

配列でも同じです。

C#
public class Sample
{
private readonly int[] numbers = { 1, 2, 3 };

public void Change()
{
numbers[0] = 999; // OK
}
}

numbersに別の配列を代入することはできません。

C#
// numbers = new int[] { 4, 5, 6 }; // エラー

しかし、配列の要素は変更できます。

C#
numbers[0] = 999; // OK

List<T>も同じです。

C#
public class Sample
{
private readonly List<int> numbers = new List<int>();

public void Add()
{
numbers.Add(10); // OK
}

public void Clear()
{
numbers.Clear(); // OK
}
}

「コレクションの中身も変更されたくない」という場合は、外部に直接List<T>や配列を公開しないようにする必要があります。

たとえば、読み取り専用の形で公開します。

C#
public class Sample
{
private readonly List<string> names = new List<string>();

public IReadOnlyList<string> Names => names;

public void AddName(string name)
{
names.Add(name);
}
}

このようにすると、外部からはNamesを読み取れますが、直接AddClearはできません。

6-3. readonlyはローカル変数には使えない

通常のローカル変数にはreadonlyを付けられません。

次のようなコードは書けません。

C#
public void Sample()
{
// readonly int number = 10; // エラー
}

readonlyは主にフィールドに使うキーワードです。

メソッド内のローカル変数を「再代入しない」ようにしたい場合は、コードの書き方やレビューで管理することになります。

C#
public void Sample()
{
int number = 10;

Console.WriteLine(number);

// number = 20; // 書けるが、設計上再代入しないようにする
}

なお、C#にはref readonlyなどの高度な機能もありますが、通常の「ローカル変数を読み取り専用にする」という意味でreadonly int numberのようには書けません。

初心者のうちは、readonlyはフィールドに使うもの、と理解しておくとよいでしょう。

6-4. プロパティのgetのみとの違いに注意する

C#では、readonlyフィールドのほかに、getのみのプロパティでも読み取り専用の値を表現できます。

C#
public class User
{
public int Id { get; }

public User(int id)
{
Id = id;
}
}

このIdプロパティは、外部から値を変更できません。

C#
var user = new User(1);

// user.Id = 2; // エラー

一見すると、readonlyフィールドと似ています。

C#
public class User
{
private readonly int id;

public int Id => id;

public User(int id)
{
this.id = id;
}
}

違いは、readonlyはフィールドに対する制約であり、getのみのプロパティは外部公開の形を制御するための仕組みだという点です。

実務では、内部ではprivate readonlyフィールドを使い、外部にはプロパティで公開する形がよく使われます。

C#
public class User
{
private readonly int id;

public int Id => id;

public User(int id)
{
this.id = id;
}
}

また、単純な値であれば、最初からgetのみの自動プロパティを使っても問題ありません。

C#
public class User
{
public int Id { get; }

public User(int id)
{
Id = id;
}
}

6-5. mutableなオブジェクトをreadonlyにする場合の落とし穴

mutableとは「変更可能」という意味です。

たとえば、List<T>や配列、自作の変更可能なクラスはmutableなオブジェクトです。

C#
public class User
{
public string Name { get; set; }
}

このUserクラスは、Nameをあとから変更できます。

これをreadonlyフィールドにしても、参照先のUserオブジェクトの中身は変更できます。

C#
public class Sample
{
private readonly User user;

public Sample(User user)
{
this.user = user;
}

public void ChangeName()
{
user.Name = "Bob"; // OK
}
}

readonlyによって防げるのは、次のような再代入です。

C#
// user = new User(); // エラー

しかし、user.Nameの変更は防げません。

「完全に変更されたくないデータ」を作りたい場合は、クラス自体を不変に設計する必要があります。

C#
public class User
{
public string Name { get; }

public User(string name)
{
Name = name;
}
}

このように、プロパティにsetを用意しないことで、外部から変更されにくい設計にできます。

6-6. 不変性を高めたい場合の設計ポイント

readonlyだけで完全な不変性を実現するのは難しい場合があります。

不変性を高めたい場合は、次のような設計を意識しましょう。

まず、フィールドはprivate readonlyにします。

C#
private readonly string name;

次に、外部には読み取り専用プロパティで公開します。

C#
public string Name => name;

コレクションを扱う場合は、List<T>をそのまま公開しないようにします。

C#
private readonly List<string> names = new List<string>();

public IReadOnlyList<string> Names => names;

また、値を変更する必要がある場合は、既存のオブジェクトを変更するのではなく、新しいオブジェクトを作る設計にすることもあります。

C#
public class User
{
public string Name { get; }

public User(string name)
{
Name = name;
}

public User ChangeName(string newName)
{
return new User(newName);
}
}

このようにすると、元のUserオブジェクトは変更されず、新しいUserオブジェクトが作られます。

readonlyは不変性を高めるための重要な手段ですが、それだけで完全にimmutableになるわけではありません。クラス全体の設計と組み合わせて使うことが大切です。

7. readonlyはいつ使うべきか

7-1. 初期化後に変更してほしくない値に使う

readonlyを使うべき基本的な場面は、初期化後に変更してほしくない値があるときです。

たとえば、次のような値です。

C#
private readonly int userId;
private readonly DateTime createdAt;
private readonly Guid orderId;

これらは、オブジェクトが作られたあとに変更されると困ることが多い値です。

C#
public class User
{
private readonly int userId;

public User(int userId)
{
this.userId = userId;
}
}

userIdが途中で変わると、「このオブジェクトは誰を表しているのか」がわかりにくくなります。

そのため、変更しない値としてreadonlyを付けるのが自然です。

7-2. オブジェクトの状態を安全に保ちたいときに使う

readonlyは、オブジェクトの状態を安全に保つためにも使えます。

たとえば、注文を表すクラスを考えます。

C#
public class Order
{
private readonly int orderId;
private readonly DateTime orderedAt;

public Order(int orderId)
{
this.orderId = orderId;
this.orderedAt = DateTime.Now;
}
}

注文IDや注文日時は、注文後に変わるべきではありません。

このような値をreadonlyにしておくことで、クラスの状態が安定します。

状態が安定すると、コードを読む人も「この値は途中で変わらない」と安心して扱えます。

7-3. 依存オブジェクトをコンストラクター注入する場面で使う

実務で非常によく使われるのが、依存オブジェクトをprivate readonlyフィールドに保持する書き方です。

C#
public class UserService
{
private readonly IUserRepository userRepository;
private readonly ILogger logger;

public UserService(IUserRepository userRepository, ILogger logger)
{
this.userRepository = userRepository;
this.logger = logger;
}

public void CreateUser(string name)
{
userRepository.Save(name);
logger.Log("ユーザーを作成しました。");
}
}

このように、コンストラクターで受け取った依存オブジェクトをreadonlyにしておくと、クラスの途中で別のリポジトリやロガーに差し替わることを防げます。

依存オブジェクトは、基本的にクラス生成時に決まり、その後は変わらないことが多いです。

そのため、コンストラクター注入とprivate readonlyは相性がよい組み合わせです。

7-4. マジックナンバーや固定値にはconstと使い分ける

コードの中に直接書かれた意味のわかりにくい数値を、マジックナンバーと呼ぶことがあります。

C#
if (count > 100)
{
Console.WriteLine("上限を超えました。");
}

この100が何を意味するのか、コードを見ただけでは少しわかりにくいです。

このような場合は、名前付きの定数として定義すると読みやすくなります。

C#
private const int MaxCount = 100;

if (count > MaxCount)
{
Console.WriteLine("上限を超えました。");
}

このように、コンパイル時に決まる単純な値で、絶対に変更されないものはconstが向いています。

一方、実行時に決まる値や、型に制限を受けたくない値はreadonlyを使います。

C#
private readonly DateTime createdAt = DateTime.Now;

クラス全体で共有したい場合はstatic readonlyです。

C#
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);

固定値だからすべてconstにするのではなく、値の性質によって使い分けることが大切です。

7-5. チーム開発で意図しない代入を防ぐために使う

チーム開発では、自分以外の人も同じコードを修正します。

そのため、「この値は変更してはいけない」という意図をコードで表すことが重要です。

C#
private readonly string apiKey;

このように書かれていれば、他の開発者も「このapiKeyは初期化後に変更しない値だ」と理解しやすくなります。

また、誤って再代入しようとしてもコンパイルエラーになります。

C#
public void Reset()
{
// apiKey = "new-key"; // エラー
}

コメントで「変更しないでください」と書くよりも、readonlyでコンパイラに守ってもらうほうが安全です。

C#
// 変更しないでください
private string apiKey;

このようなコメントだけでは、間違って代入してもエラーにはなりません。

一方、readonlyを使えば、ルールをコードとして強制できます。

C#
private readonly string apiKey;

チーム開発では、意図を明確にし、ミスを防ぐためにreadonlyを積極的に使うとよいでしょう。

8. readonlyに関するよくある疑問

8-1. readonlyを付けるとパフォーマンスは上がるのか

readonlyを付けたからといって、必ず大きくパフォーマンスが上がるわけではありません。

readonlyの主な目的は、パフォーマンス向上ではなく、意図しない再代入を防ぐことです。

C#
private readonly int id;

このように書くことで、「このフィールドは初期化後に変更しない」という意図を明確にできます。

ただし、構造体やreadonly structin引数などを使う場面では、コピーの抑制や最適化に関係することがあります。

しかし、初心者が通常のクラス設計でreadonlyを使う目的は、まず安全性と可読性の向上だと考えて問題ありません。

8-2. readonlyとprivate readonlyは何が違うのか

readonlyはフィールドの再代入を制限するキーワードです。

privateはアクセス範囲を制限するキーワードです。

つまり、役割が違います。

C#
private readonly int id;

この場合、privateによってクラスの外から直接アクセスできなくなり、readonlyによって初期化後に再代入できなくなります。

分けて考えると、次のようになります。

キーワード役割
privateクラスの外から直接アクセスできないようにする
readonly初期化後に再代入できないようにする

実務では、フィールドは外部に直接公開せず、private readonlyにすることが多いです。

C#
public class User
{
private readonly int id;

public int Id => id;

public User(int id)
{
this.id = id;
}
}

この形にすると、内部の値を安全に保持しつつ、外部には読み取り専用で公開できます。

8-3. readonlyとプロパティのgetのみはどちらを使うべきか

単純な値を外部に公開したいだけなら、getのみのプロパティがシンプルです。

C#
public class User
{
public int Id { get; }

public User(int id)
{
Id = id;
}
}

一方、内部でだけ使う依存オブジェクトや、外部に直接見せたくない値にはprivate readonlyフィールドが向いています。

C#
public class UserService
{
private readonly IUserRepository repository;

public UserService(IUserRepository repository)
{
this.repository = repository;
}
}

また、内部フィールド名と外部公開名を分けたい場合は、private readonlyフィールドとプロパティを組み合わせます。

C#
public class User
{
private readonly int id;

public int Id => id;

public User(int id)
{
this.id = id;
}
}

判断の目安は次のとおりです。

場面おすすめ
外部に値を公開したいgetのみのプロパティ
内部で依存オブジェクトを保持したいprivate readonlyフィールド
内部実装を隠して公開したいprivate readonly + 読み取り専用プロパティ

8-4. readonlyは後から値を変更できないのか

基本的に、readonlyフィールドは初期化後に変更できません。

代入できるのは、主に次のタイミングです。

・フィールド宣言時
・コンストラクター内

たとえば、これはOKです。

C#
public class Sample
{
private readonly int value = 10;
}

これもOKです。

C#
public class Sample
{
private readonly int value;

public Sample()
{
value = 10;
}
}

しかし、通常のメソッド内で変更することはできません。

C#
public class Sample
{
private readonly int value;

public Sample()
{
value = 10;
}

public void Change()
{
// value = 20; // エラー
}
}

後から値を変更する必要があるなら、readonlyを付けない設計にしましょう。

C#
public class Sample
{
private int value;

public void Change()
{
value = 20; // OK
}
}

8-5. readonlyフィールドにnullは設定できるのか

readonlyフィールドにはnullを設定できます。

C#
public class Sample
{
private readonly string? name;

public Sample()
{
name = null;
}
}

ただし、readonlynullを設定した場合、その後別の値に変更することはできません。

C#
public class Sample
{
private readonly string? name;

public Sample()
{
name = null;
}

public void SetName()
{
// name = "Alice"; // エラー
}
}

つまり、「最初はnullにしておいて、あとで値を入れる」という使い方には向いていません。

あとから値を入れる必要があるなら、readonlyを外すか、コンストラクターで必ず値を受け取る設計にします。

C#
public class User
{
private readonly string name;

public User(string name)
{
this.name = name;
}
}

C#でnull許容参照型を使っている場合は、stringstring?の違いにも注意しましょう。

C#
private readonly string name;   // nullを想定しない
private readonly string? memo; // nullを許容する

8-6. readonlyとimmutableは同じ意味なのか

readonlyimmutableは似ていますが、同じ意味ではありません。

readonlyは、フィールドへの再代入を禁止する仕組みです。

C#
private readonly List<string> names = new List<string>();

この場合、namesに別のリストを代入することはできません。

C#
// names = new List<string>(); // エラー

しかし、リストの中身は変更できます。

C#
names.Add("Alice"); // OK

一方、immutableは「オブジェクトの状態そのものが変更されない」という考え方です。

たとえば、次のようなクラスは比較的immutableに近い設計です。

C#
public class User
{
public string Name { get; }

public User(string name)
{
Name = name;
}
}

Nameにはsetがないため、外部から変更できません。

C#
var user = new User("Alice");

// user.Name = "Bob"; // エラー

つまり、readonlyは不変性を実現するための部品のひとつですが、それだけで完全にimmutableになるわけではありません。

本当に不変なオブジェクトを作りたい場合は、次のような点を意識する必要があります。

設計ポイント内容
フィールドをreadonlyにする再代入を防ぐ
プロパティをgetのみにする外部からの変更を防ぐ
mutableなオブジェクトを直接公開しないList<T>や配列の直接公開を避ける
必要ならコピーを返す外部から内部状態を変更されないようにする
値を変えるときは新しいオブジェクトを作る元の状態を保つ

readonlyimmutableを混同しないことが、C#で安全な設計をするうえで大切です。

まとめ

C#のreadonlyは、初期化後にフィールドへ再代入できないようにするためのキーワードです。

主に、ID、作成日時、設定値、依存オブジェクトなど、途中で変更されると困る値に使います。

C#
private readonly int id;
private readonly DateTime createdAt;

readonlyフィールドは、宣言時またはコンストラクター内で初期化できます。

C#
public class User
{
private readonly int id;

public User(int id)
{
this.id = id;
}
}

constとの大きな違いは、値が決まるタイミングです。

constはコンパイル時に決まる定数で、readonlyは実行時に値を決めることができます。

C#
public const int MaxCount = 100;

public readonly DateTime CreatedAt = DateTime.Now;

クラス全体で共有する読み取り専用の値には、static readonlyを使います。

C#
public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);

ただし、readonlyは参照先の中身まで不変にするわけではありません。

C#
private readonly List<string> names = new List<string>();

names.Add("Alice"); // OK

配列やList<T>readonlyにしても、要素の変更は可能です。

そのため、本当に不変な設計にしたい場合は、private readonlyフィールド、読み取り専用プロパティ、IReadOnlyList<T>、不変クラス設計などを組み合わせる必要があります。

C#のreadonlyは、コードの安全性と可読性を高めるために非常に役立つキーワードです。

「この値は初期化後に変えたくない」と思ったら、まずreadonlyを使えないか検討してみましょう。