C# readonlyとは?constとの違い・初期化タイミング・参照型の注意点を初心者向けに徹底解説

はじめに

C#のreadonlyは、フィールドの値が意図せず書き換えられることを防ぐための修飾子です。

たとえば、オブジェクトの生成時に受け取ったIDや設定値、依存オブジェクトなどは、生成後に別の値へ変更したくないことがあります。そのようなフィールドをreadonlyにすると、コンパイラが不正な再代入を検出してくれます。

ただし、readonlyについては次のような疑問も生まれやすいでしょう。

  • constとは何が違うのか

  • どのタイミングで値を代入できるのか

  • static readonlyは何のために使うのか

  • 配列やList<T>readonlyにすれば中身も変更できなくなるのか

  • 読み取り専用プロパティやreadonly structとは何が違うのか

この記事では、C#のreadonlyフィールドを中心に、基本構文、constとの違い、初期化タイミング、参照型での注意点、実践的な使い方を初心者向けに解説します。

1. C#のreadonlyとは?まず結論からわかりやすく解説

1-1. readonlyは「初期化後に再代入できない」フィールド修飾子

C#のreadonlyは、フィールドに対して使用する修飾子です。

readonlyフィールドには、フィールドの宣言時またはコンストラクタ内で値を代入できます。しかし、オブジェクトの初期化が完了した後は、通常のメソッドなどから別の値を再代入できません。

public class User{private readonly int _id;
public User(int id){_id = id;}public void ChangeId(int newId){// _id = newId;// コンパイルエラー}

}

この例では、_idへの代入はコンストラクタ内なので許可されます。一方、ChangeIdメソッドからの再代入は許可されません。

厳密には、readonlyは「最初の1回しか代入できない」という意味ではありません。宣言時に初期値を設定した後、コンストラクタで別の値を代入することも可能です。

public class Sample{private readonly int _number = 10;

public Sample(){_number = 20;}

}

このコードでは、最終的な_numberの値は20です。重要なのは、コンストラクタによる初期化が完了した後に再代入できないという点です。[1]

1-2. readonlyでできること・できないこと

readonlyでできることは、主に次のとおりです。

  • フィールド宣言時に値を代入する

  • インスタンスコンストラクタで値を代入する

  • 実行時に計算した値を保持する

  • インスタンスごとに異なる値を保持する

  • static readonlyとしてクラス全体で値を共有する

  • クラス、配列、コレクションなどの参照型を保持する

一方、次のような操作はできません。

  • 通常のメソッド内で別の値を再代入する

  • プロパティのセッター内で再代入する

  • 派生クラスのコンストラクタから基底クラスのreadonlyフィールドに代入する

  • オブジェクト初期化子からreadonlyフィールドに代入する

  • ローカル変数にreadonlyを付ける

次のコードはコンパイルエラーになります。

public class Counter{private readonly int _count;

public Counter(){_count = 0;}public void Increment(){// _count++;// readonlyフィールドへの再代入になるためエラー}

}

_count++は、内部的には現在の値を読み取り、1を加えた値を再代入する処理です。そのため、readonlyフィールドには使用できません。

1-3. readonlyが使われる主な場面

readonlyは、オブジェクトの生成後に参照先や値を変更したくないフィールドで使用します。

代表的な用途は次のとおりです。

public class Order{private readonly Guid _orderId;private readonly DateTime _createdAt;private readonly PaymentService _paymentService;

public Order(PaymentService paymentService){_orderId = Guid.NewGuid();_createdAt = DateTime.UtcNow;_paymentService = paymentService;}

}

この例では、注文ID、作成日時、決済サービスへの参照を、注文オブジェクトの生成後に差し替えられないようにしています。

ほかにも、次のような場面で利用されます。

  • コンストラクタで受け取った設定値

  • DIによって注入されたサービス

  • オブジェクト固有の識別子

  • 作成日時

  • ファイルパスや接続先情報

  • クラス全体で共有するTimeSpanや比較オブジェクト

  • 実行時に生成する定数的なオブジェクト

1-4. readonlyを使うメリット

readonlyを使用する最大のメリットは、フィールドを変更しないという設計上の意図をコードで表現できることです。

たとえば、次のフィールドだけでは、後から変更してよい値なのか判断できません。

private string _connectionString;

readonlyを付けると、初期化後には変更しない値であることが明確になります。

private readonly string _connectionString;

また、誤って値を再代入した場合はコンパイルエラーになるため、不具合を実行前に発見できます。

ただし、readonlyを付けただけでオブジェクト全体が不変になったり、スレッドセーフになったりするわけではありません。参照型を扱う場合は、参照先のオブジェクトが変更可能かどうかも考える必要があります。

2. readonlyの基本構文と書き方

2-1. readonlyフィールドの宣言方法

基本構文は次のとおりです。

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

具体例を見てみましょう。

public class Product{private readonly int _id;private readonly string _name;public readonly decimal TaxRate;}

readonlyはフィールドに付けます。メソッド内のローカル変数には使用できません。

public void Execute(){// readonly int number = 10;// ローカル変数には使用できない}

ローカル変数を変更不可の定数として扱える場合は、constを検討します。

public void Execute(){const int MaxRetryCount = 3;}

2-2. コンストラクタでreadonlyフィールドを初期化する方法

インスタンスのreadonlyフィールドは、そのフィールドを宣言したクラスのインスタンスコンストラクタで初期化できます。

public class Customer{private readonly int _id;private readonly string _name;

public Customer(int id, string name){_id = id;_name = name;}

}

コンストラクタの引数を使えるため、インスタンスごとに異なる値を設定できます。

var customer1 = new Customer(1, "佐藤");var customer2 = new Customer(2, "鈴木");

複数のコンストラクタがある場合は、それぞれのコンストラクタで異なる値を設定することも可能です。

public class Connection{private readonly int _timeoutSeconds;

public Connection(){_timeoutSeconds = 30;}public Connection(int timeoutSeconds){_timeoutSeconds = timeoutSeconds;}

}

なお、フィールドに明示的な値を代入しなかった場合は、intなら0、参照型ならnullなど、型の既定値になります。意図しない既定値を避けるため、必要なreadonlyフィールドはコンストラクタで明示的に初期化するのが安全です。

2-3. 宣言時にreadonlyフィールドを初期化する方法

すべてのインスタンスで同じ方法によって初期値を設定する場合は、フィールドの宣言時に初期化できます。

public class RequestContext{private readonly DateTime _createdAt = DateTime.UtcNow;private readonly Guid _requestId = Guid.NewGuid();}

この場合、インスタンスが生成されるたびにDateTime.UtcNowGuid.NewGuid()が評価されます。

var context1 = new RequestContext();var context2 = new RequestContext();

context1context2では、作成日時やGUIDが異なる可能性があります。

宣言時の初期値を、コンストラクタで上書きすることもできます。

public class ApiClient{private readonly int _timeoutSeconds = 30;

public ApiClient(){}public ApiClient(int timeoutSeconds){_timeoutSeconds = timeoutSeconds;}

}

引数を指定しない場合は30、引数を指定した場合はその値が使用されます。

2-4. readonlyに代入できる場所・代入できない場所

インスタンスのreadonlyフィールドに代入できる場所は、基本的に次の2か所です。

  1. フィールドの宣言時

  2. そのフィールドを宣言したクラスのインスタンスコンストラクタ

public class Example{private readonly int _value = 10;

public Example(int value){_value = value;}

}

次の場所からは代入できません。

public class Example{private readonly int _value;

public Example(){_value = 10;}public void Change(){// _value = 20;// エラー}public int Value{get =&gt; _value;set{// _value = value;// エラー}}

}

派生クラスのコンストラクタからも、基底クラスで宣言されたreadonlyフィールドには直接代入できません。

public class BaseClass{protected readonly int Value;

protected BaseClass(int value){Value = value;}

}

public class DerivedClass : BaseClass{public DerivedClass(int value) : base(value){// Value = value;// 基底クラスのreadonlyフィールドなので代入不可}}

基底クラスのコンストラクタへ値を渡して初期化するのが正しい方法です。

3. readonlyとconstの違い

3-1. constはコンパイル時定数、readonlyは実行時に初期化できる値

constreadonlyの最も大きな違いは、値が決定されるタイミングです。

constは、コンパイル時に値が確定している必要があります。

public const int MaxRetryCount = 3;public const string ApplicationName = "SampleApp";

一方、readonlyはプログラムの実行時に値を決定できます。

public readonly DateTime CreatedAt = DateTime.UtcNow;public readonly Guid Id = Guid.NewGuid();

DateTime.UtcNowGuid.NewGuid()の結果は、プログラムを実行するまで決まりません。そのため、constにはできませんが、readonlyにはできます。

また、constフィールドは暗黙的にstaticとして扱われます。インスタンスを生成しなくてもクラス名からアクセスできます。

public class AppSettings{public const int MaxRetryCount = 3;}

Console.WriteLine(AppSettings.MaxRetryCount);

3-2. constとreadonlyの初期化タイミングの違い

constは、宣言と同時に定数式で初期化しなければなりません。

public const int MaxCount = 100;

次のように、宣言と初期化を分けることはできません。

public class Example{// public const int MaxCount;

public Example(){// MaxCount = 100;}

}

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

public class Example{private readonly int _maxCount;

public Example(int maxCount){_maxCount = maxCount;}

}

この違いにより、外部から受け取った値、設定ファイルから読み込んだ値、現在日時などはreadonlyで保持できます。

3-3. constとreadonlyで使える型の違い

constで使用できる型には制限があります。

一般的には、数値型、boolcharstring、列挙型など、コンパイル時に値を確定できる型で使用します。クラス、構造体、配列などのユーザー定義型やオブジェクトは、基本的にconstにできません。参照型については、stringまたはnull参照などに限られます。[2]

public const int MaxCount = 100;public const bool IsEnabled = true;public const char Separator = ',';public const string AppName = "Sample";

次の宣言はできません。

// public const DateTime StartDate = DateTime.UtcNow;// public const Guid ApplicationId = Guid.NewGuid();// public const int[] Numbers = new[] { 1, 2, 3 };

readonlyには、フィールドとして使用できるほぼすべての型を指定できます。

public readonly DateTime StartDate = DateTime.UtcNow;public readonly Guid ApplicationId = Guid.NewGuid();public readonly int[] Numbers = { 1, 2, 3 };

ただし、配列などの参照型をreadonlyにしても、要素まで変更できなくなるわけではありません。

3-4. constとreadonlyの使い分け早見表

比較項目constreadonly
値の決定時期コンパイル時実行時でも可能
宣言時の初期化必須任意
コンストラクタでの代入不可可能
インスタンスごとの値不可可能
暗黙的にstaticかはいいいえ
参照型の利用大きな制限がある利用可能
DateTime.Nowの利用不可可能
newによるオブジェクト生成不可可能
ローカル変数での利用可能不可
初期化後の再代入不可不可

基本的には、「コンパイル時に完全に決まる単純な値」ならconst、「実行時に決まる値やオブジェクト」ならreadonlyと考えるとよいでしょう。

3-5. どちらを使うべきか判断する具体例

最大試行回数のように、ソースコード上で固定される整数にはconstが適しています。

public const int MaxRetryCount = 3;

実行時に生成する期間オブジェクトにはstatic readonlyが適しています。

public static readonly TimeSpan DefaultTimeout =TimeSpan.FromSeconds(30);

インスタンスごとに異なる値には、通常のreadonlyを使用します。

public class User{private readonly int _id;

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

}

公開ライブラリの値については、将来変更する可能性があるならpublic constを安易に使わないことも重要です。constの値は利用側のコードに埋め込まれるため、ライブラリ側で値を変更しても、利用側を再コンパイルしなければ古い値が使われる場合があります。[2]

public static readonly int DefaultPageSize = 20;

変更の可能性がある公開値には、static readonlyや読み取り専用プロパティを選ぶ方法があります。

4. readonlyの初期化タイミングを理解する

4-1. フィールド宣言時に初期化されるケース

次のコードでは、インスタンスの生成時にフィールド初期化子が評価されます。

public class Session{private readonly Guid _sessionId = Guid.NewGuid();private readonly DateTime _startedAt = DateTime.UtcNow;}

Sessionを生成するたびに、新しいGUIDと現在日時が設定されます。

var session1 = new Session();var session2 = new Session();

すべてのインスタンスで同じ値になるわけではありません。staticが付いていないフィールドは、インスタンスごとに作られるためです。

宣言時の初期化は、次のような値に向いています。

  • 現在日時

  • 新しいGUID

  • 空のコレクション

  • 既定の設定値

  • インスタンスごとに生成する補助オブジェクト

4-2. インスタンスコンストラクタで初期化されるケース

外部から受け取った値を保持する場合は、コンストラクタで初期化します。

public class Report{private readonly string _title;private readonly DateTime _targetDate;

public Report(string title, DateTime targetDate){_title = title;_targetDate = targetDate;}

}

コンストラクタで初期化することで、「このオブジェクトを作るために必要な値」を明確にできます。

また、入力値を検証したうえで代入することも可能です。

public class RetryPolicy{private readonly int _maxRetryCount;

public RetryPolicy(int maxRetryCount){if (maxRetryCount &lt; 0){throw new ArgumentOutOfRangeException(nameof(maxRetryCount));}_maxRetryCount = maxRetryCount;}

}

このように、正しい値だけをreadonlyフィールドへ保存すれば、その後に不正な値へ変更されることを防げます。

4-3. static readonlyが静的コンストラクタで初期化されるケース

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

public class ApplicationInfo{public static readonly string InstanceId;

static ApplicationInfo(){InstanceId = Guid.NewGuid().ToString();}

}

静的コンストラクタは、最初のインスタンスが生成される前や、静的メンバーが最初に参照される前に、自動的に実行されます。また、基本的に1回だけ呼び出されます。[3]

複数の処理を行ってから値を決定したい場合は、静的コンストラクタが便利です。

public class EnvironmentSettings{public static readonly string EnvironmentName;

static EnvironmentSettings(){string? value =Environment.GetEnvironmentVariable("APP_ENV");EnvironmentName =string.IsNullOrWhiteSpace(value)? "Production": value;}

}

単純な式で初期化できる場合は、宣言時に記述した方が簡潔です。

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

4-4. 初期化後に再代入できない理由

readonlyフィールドは、オブジェクトが持つ重要な前提条件を維持するために使われます。

たとえば、注文IDが途中で変更できると、ログ、データベース、決済情報などの対応関係が崩れる可能性があります。

public class Order{private readonly Guid _id;

public Order(Guid id){_id = id;}

}

_idreadonlyにすることで、注文オブジェクトが存在している間は同じIDを保持できます。

コンパイラが代入可能な場所を制限するため、コードレビューだけに頼らず、設計上のルールを強制できることがreadonlyの利点です。

4-5. 初期化タイミングを間違えたときのエラー例

通常のメソッドから代入すると、コンパイルエラーになります。

public class Sample{private readonly int _value;

public Sample(){_value = 10;}public void Reset(){// _value = 0;// CS0191}

}

static readonlyフィールドをインスタンスコンストラクタで初期化することもできません。

public class Sample{private static readonly int _value;

public Sample(){// _value = 10;// エラー}

}

静的コンストラクタを使用します。

public class Sample{private static readonly int _value;

static Sample(){_value = 10;}

}

また、フィールドとコンストラクタ引数の名前が似ている場合は、フィールドに代入したつもりでローカル変数を操作してしまわないよう注意しましょう。

public class User{private readonly string _name;

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

}

thisを使用する命名方法なら、次のようにも書けます。

public class User{private readonly string name;

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

}

5. readonlyとstatic readonlyの違い

5-1. readonlyはインスタンスごとに値を持つ

通常のreadonlyフィールドは、インスタンスごとに作られます。

public class User{public readonly int Id;

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

}

それぞれのインスタンスに異なる値を設定できます。

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

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

ユーザーID、注文ID、作成日時など、オブジェクトごとに異なる値には通常のreadonlyが適しています。

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

static readonlyフィールドは、インスタンスではなく型そのものに属します。

public class ApiSettings{public static readonly TimeSpan DefaultTimeout =TimeSpan.FromSeconds(30);}

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

Console.WriteLine(ApiSettings.DefaultTimeout);

static readonlyフィールドは、アプリケーション内でクラスごとに1つの値を共有します。

public class TokenService{public static readonly string Algorithm = LoadAlgorithm();

private static string LoadAlgorithm(){return "SHA256";}

}

5-3. static readonlyがよく使われるケース

static readonlyは、クラス全体で共有したい次のような値に使われます。

  • TimeSpanなどの値オブジェクト

  • 日付やGUID

  • 比較オブジェクト

  • 正規表現オブジェクト

  • 実行時に読み込む設定値

  • 不変コレクション

  • ファクトリーメソッドで生成するオブジェクト

public static class ValidationSettings{public static readonly TimeSpan CacheDuration =TimeSpan.FromMinutes(10);

public static readonly StringComparer UserNameComparer =StringComparer.OrdinalIgnoreCase;

}

ただし、変更可能なオブジェクトをpublic static readonlyで公開すると、そのオブジェクトの中身を外部から変更される可能性があります。

public static readonly List<string> Names = new();

このフィールドは別のList<string>へ差し替えられませんが、次の操作は可能です。

Names.Add("佐藤");

共有データには、不変コレクションなどを使う方が安全です。

5-4. constではなくstatic readonlyを選ぶべき場面

次のような値にはstatic readonlyを選びます。

  • コンパイル時に値が決まらない

  • newを使ってオブジェクトを生成する

  • メソッドの戻り値で初期化する

  • DateTimeGuidTimeSpanなどを使用する

  • 配列やコレクションを保持する

  • 将来変更する可能性がある公開値

public static readonly DateTime StartedAt =DateTime.UtcNow;

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

public static readonly TimeSpan DefaultTimeout =TimeSpan.FromSeconds(30);

文字列や数値であっても、環境変数などから実行時に取得する場合はconstにできません。

public static readonly string EnvironmentName =Environment.GetEnvironmentVariable("APP_ENV")?? "Production";

6. readonlyと参照型の注意点

6-1. readonlyは参照先を変更できないだけでオブジェクトの中身は変更できる

参照型をreadonlyにした場合、変更できなくなるのはフィールドが保持する参照です。参照先のオブジェクト自体が変更可能であれば、そのプロパティや状態は変更できます。

public class User{public string Name { get; set; } = "";}

public class UserService{private readonly User _user = new();

public void Update(){_user.Name = "佐藤"; // 可能// _user = new User();// 別オブジェクトへの再代入はエラー}

}

_userが常に同じUserオブジェクトを参照することは保証されます。しかし、User.Nameが変更されないことまでは保証されません。

この違いは、次のように考えると理解しやすくなります。

  • 値型のreadonly:フィールドの値を再代入できない

  • 参照型のreadonly:フィールドが指す参照先を差し替えられない

  • 参照先オブジェクトの変更可否:そのオブジェクトの設計によって決まる

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

配列をreadonlyにしても、要素は変更できます。

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

public void Update(){_numbers<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[0]</span> = 100; // 可能// _numbers = new[] { 4, 5, 6 };// 再代入はエラー}

}

List<T>も同様です。

public class Sample{private readonly List<string> _names = new();

public void AddName(string name){_names.Add(name); // 可能}

}

readonlyが制限しているのは、次の再代入です。

// _names = new List<string>();

AddRemoveClearなど、同じリストオブジェクトの内容を変更する操作は制限されません。

6-3. 参照型readonlyで起こりやすい勘違い

参照型のreadonlyについて、特に多い勘違いは次の3つです。

1つ目は、「readonlyならオブジェクトの中身も変わらない」という勘違いです。

private readonly List<int> _values = new();

このリストには要素を追加できます。

2つ目は、「readonlyならスレッドセーフになる」という勘違いです。

複数のスレッドから同じ変更可能なリストを操作すると、フィールドがreadonlyでも競合が発生する可能性があります。

3つ目は、「呼び出し元から渡されたオブジェクトがコピーされる」という勘違いです。

public class SettingsHolder{private readonly List<string> _values;

public SettingsHolder(List&lt;string&gt; values){_values = values;}

}

このコードでは、渡されたリストへの参照が保存されます。呼び出し元が元のリストを変更すると、_valuesから見える内容も変化します。

6-4. オブジェクト自体を変更不可にしたい場合の対策

オブジェクト自体を変更不可にしたい場合は、readonlyだけでなく、参照先の型も不変に設計します。

public sealed class User{public int Id { get; }public string Name { get; }

public User(int id, string name){Id = id;Name = name;}

}

このクラスにはセッターや状態変更メソッドがないため、生成後にIdNameを変更できません。

コレクションを扱う場合は、外部から受け取ったデータをコピーする方法もあります。

public class Team{private readonly List<string> _members;

public Team(IEnumerable&lt;string&gt; members){_members = new List&lt;string&gt;(members);}public IReadOnlyList&lt;string&gt; Members =&gt; _members;

}

ただし、IReadOnlyList<T>は「その参照を通して変更操作を公開しない」というインターフェイスです。元となるList<T>が変更されれば、表示される内容も変化します。

完全に変更できないデータが必要なら、不変コレクションを検討します。

6-5. Immutableコレクションや読み取り専用コレクションの活用

.NETには、変更できないコレクションを扱うためのSystem.Collections.Immutableが用意されています。[6]

using System.Collections.Immutable;

public class RoleSettings{public static readonly ImmutableArray<string> DefaultRoles =ImmutableArray.Create("User", "Viewer");}

ImmutableArray<T>ImmutableList<T>では、既存のインスタンスの内容を直接変更できません。要素を追加する操作を行うと、新しいコレクションが返されます。

var values = ImmutableList.Create(1, 2, 3);var updated = values.Add(4);

Console.WriteLine(values.Count); // 3Console.WriteLine(updated.Count); // 4

一方、ReadOnlyCollection<T>は、元のリストを読み取り専用の形式で公開するラッパーです。ラッパー経由で追加や削除はできませんが、元のリストが変更されれば、その変更が読み取り専用コレクションにも反映されます。[7]

var source = new List<string> { "A" };var readOnly = source.AsReadOnly();

source.Add("B");

Console.WriteLine(readOnly.Count); // 2

両者の違いを整理すると、次のようになります。

種類特徴
readonly List<T>参照を差し替えられないが、要素は変更可能
IReadOnlyList<T>変更操作を公開しないが、元データは変わる可能性がある
ReadOnlyCollection<T>変更可能なコレクションを包む読み取り専用ビュー
ImmutableList<T>など既存インスタンスの内容を変更できない

7. readonlyとプロパティ・readonly structの違い

7-1. getのみプロパティとの違い

readonlyフィールドと、getのみのプロパティは似ていますが、役割が異なります。

public class User{private readonly int _id;

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

}

readonlyはフィールドの再代入を制限します。一方、getのみのプロパティは、クラスの利用者に読み取りだけを許可するための公開インターフェイスです。

一般的には、フィールドを直接公開せず、プロパティを通して値を公開します。

public class User{private readonly int _id;

public int Id =&gt; _id;public User(int id){_id = id;}

}

自動実装のgetのみプロパティなら、フィールドを明示せずに書けます。

public class User{public int Id { get; }

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

}

単に値を公開するだけなら、getのみの自動実装プロパティの方が簡潔です。クラス内部だけで使用する値や、実装上フィールドとして保持したい値にはreadonlyフィールドを使用します。

7-2. initアクセサとの違い

initアクセサを持つプロパティは、オブジェクトを構築している間だけ呼び出し元から値を設定できます。[4]

public class User{public int Id { get; init; }public string Name { get; init; } = "";}

オブジェクト初期化子を使用できます。

var user = new User{Id = 1,Name = "佐藤"};

生成後の代入はできません。

// user.Name = "鈴木";// エラー

readonlyフィールドは、基本的にクラス自身の宣言時またはコンストラクタで初期化します。initプロパティは、オブジェクトを作る側が初期値を指定しやすい点が特徴です。

機能主な初期化場所外部への公開
readonlyフィールド宣言時・コンストラクタ通常は非公開
getのみプロパティ宣言時・コンストラクタ読み取り専用で公開
initプロパティ宣言時・コンストラクタ・オブジェクト初期化子初期化時だけ設定可能

7-3. readonly structとは何か

readonly structは、構造体全体を読み取り専用として宣言する機能です。[5]

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

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

}

readonly structでは、インスタンスの状態を変更するようなフィールドやメンバーを持たない設計になります。

var point = new Point(10, 20);

PointXYは、生成後に変更できません。

ただし、readonly structのフィールドに変更可能な参照型を保持した場合、参照先の内容まで自動的に不変になるわけではありません。ここでも、参照型におけるreadonlyの注意点は同じです。

7-4. readonlyメンバーとの違い

構造体では、個別のメソッドやプロパティにreadonlyを付けることもできます。

public struct Point{public double X;public double Y;

public readonly double GetLength(){return Math.Sqrt(X * X + Y * Y);}

}

このreadonlyは、GetLengthメソッドが構造体自身の状態を変更しないことを表します。

readonly structは構造体全体を読み取り専用にする機能です。一方、readonlyメンバーは、通常の構造体に含まれる特定のメンバーだけが状態を変更しないことを示します。[1][5]

7-5. 初心者が混同しやすいreadonly関連機能の整理

機能対象意味
readonlyフィールドクラス・構造体のフィールド宣言時またはコンストラクタでのみ代入可能
static readonly静的フィールド型全体で共有し、初期化後は再代入不可
getのみプロパティプロパティ呼び出し元には読み取りだけを許可
initプロパティプロパティオブジェクト構築時だけ設定可能
readonly struct構造体構造体全体を読み取り専用として設計
readonlyメンバー構造体のメンバーそのメンバーが構造体の状態を変更しない
IReadOnlyList<T>コレクションの公開API変更操作をインターフェイスとして公開しない
不変コレクションコレクション既存インスタンスの内容を変更できない

同じ「読み取り専用」という表現でも、制限する対象が異なります。何を変更できなくしたいのかを考えて選ぶことが重要です。

8. readonlyの実践的な使い方

8-1. 設定値をreadonlyで保持する例

インスタンスごとに異なる設定値を、生成後に変更したくない場合はreadonlyが適しています。

public class ApiClient{private readonly string _baseUrl;private readonly TimeSpan _timeout;

public ApiClient(string baseUrl, TimeSpan timeout){if (string.IsNullOrWhiteSpace(baseUrl)){throw new ArgumentException("URLを指定してください。",nameof(baseUrl));}_baseUrl = baseUrl;_timeout = timeout;}

}

設定値が途中で変更されないため、メソッドごとに異なる接続先やタイムアウトが使われる事故を防ぎやすくなります。

public async Task<string> GetAsync(HttpClient httpClient,string path){httpClient.Timeout = _timeout;

return await httpClient.GetStringAsync($"{_baseUrl}/{path}");

}

8-2. コンストラクタ注入された依存オブジェクトをreadonlyにする例

DIで受け取ったサービスは、readonlyフィールドに保存するのが一般的です。

public interface IUserRepository{User? FindById(int id);}

public class UserService{private readonly IUserRepository _userRepository;

public UserService(IUserRepository userRepository){_userRepository =userRepository?? throw new ArgumentNullException(nameof(userRepository));}public User? FindUser(int id){return _userRepository.FindById(id);}

}

_userRepositoryreadonlyにすると、サービスの生成後に別のリポジトリへ差し替えられることを防げます。

ただし、リポジトリオブジェクト内部の状態まで変更不可になるわけではありません。保証されるのは、_userRepositoryフィールドが別のオブジェクトを参照しないことです。

8-3. 日時やGUIDなど実行時に決まる値をreadonlyにする例

作成日時や識別子は実行時に決まるため、constではなくreadonlyを使用します。

public class AuditLog{public readonly Guid Id;public readonly DateTime CreatedAt;

public AuditLog(){Id = Guid.NewGuid();CreatedAt = DateTime.UtcNow;}

}

プロパティとして公開するなら、次のようにも書けます。

public class AuditLog{public Guid Id { get; }public DateTime CreatedAt { get; }

public AuditLog(){Id = Guid.NewGuid();CreatedAt = DateTime.UtcNow;}

}

クラス内部だけで利用するならreadonlyフィールド、外部へ公開するならgetのみプロパティを選ぶと分かりやすくなります。

8-4. 定数的に使いたいオブジェクトをstatic readonlyにする例

TimeSpanや比較オブジェクトのように、クラス全体で共有したいオブジェクトにはstatic readonlyを使用できます。

public static class CommonSettings{public static readonly TimeSpan DefaultTimeout =TimeSpan.FromSeconds(30);

public static readonly StringComparer NameComparer =StringComparer.OrdinalIgnoreCase;

}

不変コレクションを共有する例は次のとおりです。

using System.Collections.Immutable;

public static class SupportedFormats{public static readonly ImmutableArray<string> Values =ImmutableArray.Create("json","xml","csv");}

変更可能な配列やList<T>public static readonlyで公開すると、中身を変更される危険があります。

public static readonly List<string> Values = new();

定数的な共有データには、不変コレクションを使用する方が安全です。

9. readonlyでよくあるエラーと解決方法

9-1. コンストラクタ以外で代入しようとしてエラーになるケース

次のコードでは、通常のメソッドからreadonlyフィールドへ代入しているため、CS0191などのコンパイルエラーになります。[8]

public class Sample{private readonly int _value;

public Sample(){_value = 10;}public void ChangeValue(){// _value = 20;}

}

生成後に値を変更する必要があるなら、readonlyを外す必要があります。

private int _value;

一方、本来変更してはいけない値なら、代入処理を削除し、変更後の値をローカル変数として扱います。

public int Calculate(){int calculatedValue = _value + 10;return calculatedValue;}

9-2. constにできない型をconstで宣言してしまうケース

次のような値は、実行時に決まるためconstにできません。

// public const DateTime CreatedAt = DateTime.UtcNow;// public const Guid Id = Guid.NewGuid();// public const TimeSpan Timeout = TimeSpan.FromSeconds(30);

static readonlyへ変更します。

public static readonly DateTime CreatedAt =DateTime.UtcNow;

public static readonly Guid Id =Guid.NewGuid();

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

インスタンスごとに異なる値が必要なら、staticを付けません。

public readonly Guid Id = Guid.NewGuid();

9-3. readonlyなのに値が変わったように見えるケース

参照型の中身が変更されると、readonlyフィールドの値が変わったように見えることがあります。

public class Sample{private readonly List<int> _numbers =new() { 1, 2, 3 };

public void Update(){_numbers<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[0]</span> = 100;}

}

このコードで変更されたのは、フィールドが保持する参照ではなく、参照先のリストの内容です。

別のリストを代入する操作はできません。

// _numbers = new List<int>();

要素も変更不可にしたい場合は、不変コレクションを利用します。

using System.Collections.Immutable;

private readonly ImmutableArray<int> _numbers =ImmutableArray.Create(1, 2, 3);

9-4. static readonlyの初期化場所を間違えるケース

static readonlyフィールドは、インスタンスコンストラクタでは初期化できません。

public class Sample{private static readonly string _name;

public Sample(){// _name = "Sample";// エラー}

}

宣言時に初期化します。

private static readonly string _name = "Sample";

または、静的コンストラクタを使用します。

public class Sample{private static readonly string _name;

static Sample(){_name = LoadName();}private static string LoadName(){return "Sample";}

}

通常のreadonlystatic readonlyでは、代入できるコンストラクタが異なります。

フィールド代入できる場所
readonly宣言時・インスタンスコンストラクタ
static readonly宣言時・静的コンストラクタ

9-5. エラーを防ぐためのチェックポイント

readonlyを使用するときは、次の点を確認しましょう。

  • 値はインスタンスごとか、クラス全体で共有するか

  • コンパイル時に決まる値ならconstを使えないか

  • 実行時に決まる値ならreadonlyまたはstatic readonlyになっているか

  • インスタンスのreadonlyを通常のコンストラクタで初期化しているか

  • static readonlyを静的コンストラクタで初期化しているか

  • 通常のメソッドから再代入していないか

  • 参照型の中身が変更可能であることを理解しているか

  • 配列やList<T>を本当に変更不可にする必要がないか

  • 外部公開する値にはプロパティが適していないか

  • 共有コレクションには不変コレクションを使えないか

特に重要なのは、「フィールドの再代入を防ぎたいのか」「参照先オブジェクトの変更も防ぎたいのか」を区別することです。

まとめ

C#のreadonlyは、フィールドを宣言時またはコンストラクタで初期化し、その後の再代入を禁止する修飾子です。

通常のreadonlyはインスタンスごとに値を持ち、static readonlyはクラス全体で1つの値を共有します。コンパイル時に値が確定する単純な定数にはconst、実行時に決まる値やオブジェクトにはreadonlyまたはstatic readonlyを使用するのが基本です。

ただし、参照型のreadonlyが禁止するのは参照先の差し替えです。配列の要素、List<T>の内容、オブジェクトのプロパティなどは変更できる場合があります。オブジェクトやコレクションの中身まで変更不可にしたい場合は、不変クラス、getのみプロパティ、initプロパティ、Immutableコレクションなどを組み合わせる必要があります。

最後に、使い分けを簡単に整理します。

  • コンパイル時に決まる値:const

  • インスタンスごとに決まり、生成後は変えない値:readonly

  • 実行時に決まり、クラス全体で共有する値:static readonly

  • 外部へ読み取り専用で公開する値:getのみプロパティ

  • オブジェクト生成時に外部から設定させる値:initプロパティ

  • 既存の内容も変更させたくないコレクション:Immutableコレクション

readonlyを適切に使用すると、コードの意図が明確になり、予期しない再代入による不具合をコンパイル時に防ぎやすくなります。