C#プログラミングにおいて、オブジェクトの状態を適切に管理し、意図しない値の書き換えを防ぐことは、堅牢なアプリケーションを構築する上での基本です。

その中心的な役割を担うのがreadonly修飾子です。

本記事では、readonlyの基本的な使い方から、混同されやすいconstとの違い、さらには近年のC#アップデートで追加された高度な活用法まで、プロフェッショナルな視点で徹底的に解説します。

readonly修飾子とは何か

C#のreadonlyは、フィールドに対して「初期化時以外は値を変更できない」という制約を付与するためのキーワードです。

これにより、プログラムの実行中に変数の値が不意に書き換わるバグを防止し、不変性(Immutability)を確保することができます。

通常、変数は宣言された後、プログラムのどこからでも再代入が可能です。

しかし、readonlyを付与されたフィールドは、宣言時の初期化またはコンストラクタ内でのみ値を設定することが許可されます。

一度コンストラクタを抜けると、その値はオブジェクトの寿命が尽きるまで固定されます。

readonlyの基本的な構文

まずは、もっとも一般的なクラスフィールドへの適用例を見てみましょう。

C#
using System;

namespace ReadonlyExample
{
    public class User
    {
        // 宣言時に初期化する例
        private readonly string _id = Guid.NewGuid().ToString();

        // コンストラクタで初期化する例
        private readonly string _name;

        public User(string name)
        {
            // コンストラクタ内であれば代入が可能
            _name = name;
        }

        public void DisplayInfo()
        {
            Console.WriteLine($"ID: {_id}, Name: {_name}");
        }

        public void UpdateName(string newName)
        {
            // 以下のコードはコンパイルエラーになります
            // _name = newName; 
        }
    }

    class Program
    {
        static void Main()
        {
            var user = new User("Alice");
            user.DisplayInfo();
        }
    }
}
実行結果
ID: (生成されたGUID), Name: Alice

この例では、_id_namereadonlyを付与しています。

これらはコンストラクタの処理が終わった後は読み取り専用となり、UpdateNameメソッドのような他の場所から変更しようとすると、コンパイラがエラーを報告します。

readonlyとconstの違いを正しく理解する

C#には、値を固定するためのキーワードとしてconstも存在します。

一見似ている両者ですが、その性質は大きく異なります。

「いつ値が決まるのか」「どのようにメモリへ配置されるか」という点が重要な分かれ目です。

決定的な5つの違い

以下の表に、readonlyconstの主な違いをまとめました。

特徴readonlyconst
評価タイミング実行時(ランタイム)コンパイル時
初期化場所宣言時またはコンストラクタ宣言時のみ
型制限任意の型(クラス、構造体を含む)組み込み型(int, double, string等)のみ
静的/インスタンス両方可能(デフォルトはインスタンス)常に暗黙的に static
バージョニングアセンブリを跨いでも安全値の変更時に参照元も再コンパイルが必要

実行時定数(readonly)とコンパイル時定数(const)

constはコンパイル時に値が確定し、その値がコード内に直接埋め込まれます。

そのため、計算結果や外部からの入力値をconstに代入することはできません。

一方、readonlyは実行時に値が決定されるため、動的に生成された値を保持するのに適しています。

C#
public class Configuration
{
    // コンパイル時に決まる値
    public const string Version = "1.0.0";

    // 実行時に決まる値(現在の時刻など)
    public readonly DateTime BootTime = DateTime.Now;
}

バージョニング問題(注意点)

特にライブラリを開発する場合、constの使用には注意が必要です。

ライブラリ側のconstの値を変更しても、それを利用しているアプリケーション側を再コンパイルしない限り、古い値が使われ続けるという現象が発生します。

これは、constが利用側のバイナリに直接埋め込まれるためです。

このようなリスクを避けるため、パブリックに公開する定数にはstatic readonlyを使用するのが一般的です。

readonly struct によるパフォーマンス最適化

C# 7.2 以降、構造体(struct)全体に対してreadonlyを付与できるようになりました。

これがreadonly structです。

通常、構造体は値渡しされる際にコピーが発生しますが、構造体のメンバを変更する可能性があると判断されると、コンパイラは安全のために「防御的コピー(Defensive Copy)」を作成することがあります。

これが頻繁に発生するとパフォーマンスの低下を招きます。

readonly structを定義することで、その構造体がいかなる場合も不変であることをコンパイラに保証し、不必要なコピーを抑制することができます。

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

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

    // すべてのメンバが読み取り専用である必要がある
    public double DistanceFromOrigin() => Math.Sqrt(X * X + Y * Y);
}

readonly struct内のフィールドはすべてreadonlyでなければならず、プロパティも set を持たない(または init のみの)形式にする必要があります。

readonlyメンバ(部分的な不変性)

C# 8.0からは、構造体全体ではなく、特定のメソッドやプロパティに対してのみreadonly修飾子を付与できるようになりました。

これにより、「このメソッドを呼び出してもオブジェクトの状態は変化しない」ことを明示できます。

C#
public struct Rect
{
    public double Width { get; set; }
    public double Height { get; set; }

    // このプロパティは状態を変更しないことを保証
    public readonly double Area => Width * Height;

    // このメソッドは状態を変更しない
    public readonly override string ToString() => $"Width: {Width}, Height: {Height}";
}

もし、readonlyが付与されたメソッド内でフィールドの値を書き換えようとすると、コンパイルエラーが発生します。

これは、構造体の不変な設計をより細粒度で制御するために非常に有効です。

ref readonly と in パラメータ

C#の進化に伴い、パフォーマンス向上のための参照渡しの機能が強化されました。

その中でもreadonlyが関わる重要な機能が2つあります。

in パラメータ

メソッドの引数にinキーワードを付けると、引数が参照渡しされつつ、メソッド内での書き換えが禁止されます。

C#
public void ProcessLargeStruct(in MyLargeStruct data)
{
    // data.Value = 10; // エラー: 読み取り専用のため変更不可
    Console.WriteLine(data.Value);
}

大きな構造体を引き渡す際、コピーコストを抑えつつ、安全性を確保したい場合に利用されます。

ref readonly 戻り値

メソッドの戻り値として参照を返しつつ、呼び出し側にその変更を許さない仕組みです。

C#
private Point _origin = new Point(0, 0);

public ref readonly Point GetOrigin()
{
    return ref _origin;
}

これにより、大きなデータのコピーを避けつつ、カプセル化を維持することができます。

readonlyとプロパティ:init専用セッター

C# 9.0で導入されたinitアクセサは、readonlyフィールドの概念をプロパティに拡張したものです。

initを使うことで、オブジェクトの初期化時のみ値を設定でき、その後は変更不能にすることができます。

C#
public class Product
{
    // オブジェクト初期化子でも設定可能だが、その後は変更不可
    public string Name { get; init; }
    public decimal Price { get; init; }
}

// 使い方
var p = new Product { Name = "Laptop", Price = 150000 };
// p.Price = 160000; // コンパイルエラー

従来のreadonlyフィールドはコンストラクタでしか値を設定できませんでしたが、initプロパティにより、オブジェクト初期化子(Object Initializer)を利用した柔軟なコード記述が可能になりました。

実践的な活用シーンとベストプラクティス

readonlyをいつ、どこで使うべきかについて、具体的な指針を解説します。

1. 依存性の注入(DI)での活用

現代的なC#開発において、DI(Dependency Injection)は欠かせません。

コンストラクタで注入されたサービスは、クラス内で変更されるべきではないため、必ずreadonlyにすべきです。

C#
public class OrderService
{
    private readonly IRepository _repository;
    private readonly ILogger _logger;

    public OrderService(IRepository repository, ILogger logger)
    {
        _repository = repository;
        _logger = logger;
    }
}

このように記述することで、誤って_repositoryに別のインスタンスを代入してしまうミスを未然に防げます。

2. スレッドセーフな設計

マルチスレッド環境において、複数のスレッドから共有されるオブジェクトが書き換え可能であると、競合状態(Race Condition)の原因になります。

readonlyを利用してイミュータブル(不変)なクラスを作成することは、スレッドセーフを確保するためのもっともシンプルで強力な戦略です。

3. マジックナンバーの排除(static readonly)

コード内で何度も登場する固定値は、static readonlyとして定義します。

constでも可能ですが、前述のバージョニング問題や、型の柔軟性を考慮するとstatic readonlyの方が推奨されるケースが多いです。

C#
public static class AppConstants
{
    public static readonly string CacheKeyPrefix = "app_cache_";
    public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
}

最新のC#(C# 12以降)におけるreadonlyの立ち位置

C# 12で導入されたプライマリコンストラクタ(Primary Constructors)では、クラス定義の行で引数を受け取ることができます。

ここで受け取ったパラメータをフィールドとして保持する場合、デフォルトではreadonlyにはなりません。

C#
// C# 12 プライマリコンストラクタ
public class Service(IDatabase db)
{
    public void Query() => db.Execute("..."); 
}

このdbはクラス内のメソッドで自由に使用できますが、明示的にreadonlyフィールドに代入しない限り、再代入が可能です。

設計意図として不変性を保ちたい場合は、依然として従来のreadonlyフィールドを明示的に定義する手法も併用されます。

また、最新のランタイムではreadonlyによる最適化が進んでおり、特にJITコンパイラが「この値は変わらない」と判断することで、より効率的なマシンコードを生成するようになっています。

まとめ

C#のreadonly修飾子は、単に「値を書き換えられないようにする」だけの機能ではありません。

それは、コードの意図を明確にし、実行時の安全性を高め、さらにはパフォーマンスの最適化までをもたらす強力なツールです。

本記事で解説した主なポイントを振り返ります。

  • readonlyとconstの違い:実行時に値が決まるか、コンパイル時に決まるか。
  • 不変性の確保:コンストラクタ以降の変更を禁止し、予期せぬ副作用を防ぐ。
  • 構造体の最適化readonly structによる防御的コピーの削減。
  • モダンなC#機能initプロパティやinパラメータとの組み合わせ。

プログラミングにおける不変性の重要性は年々高まっています。

新しくクラスや構造体を定義する際は、まず「このフィールドはreadonlyにできないか?」を検討する習慣をつけることが、より高品質なC#コードを書くための第一歩となるでしょう。