閉じる

【C#】readonlyの使い方!変数の書き換え禁止とconstとの違いを徹底解説

C#で堅牢なプログラムを開発する際、データの状態を不用意に変更させない「不変性(イミュータビリティ)」の確保は非常に重要なテーマです。

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

変数の値を固定し、意図しない書き換えを防ぐことで、バグの混入を未然に防ぎ、コードの可読性や保守性を劇的に向上させることができます。

本記事では、readonlyの基本的な使い方から、混同されやすいconstとの決定的な違い、さらには最新のC#における応用的な活用術まで、図解を交えて徹底的に解説します。

プログラミング初心者から中級者まで、正しい知識を身につけて安全なコードを書けるようになりましょう。

readonlyの基本概念と書き換え禁止の仕組み

C#におけるreadonlyは、フィールドに対して適用される修飾子で、「一度値が決まったら二度と変更できない」という制約を付与します。

変数宣言時、またはクラスのコンストラクタ内でのみ値を代入することが許可されます。

readonlyフィールドの宣言と初期化

readonlyを使用する場合、主に2つのタイミングで値を設定することができます。

一つは宣言と同時に初期値を代入する方法、もう一つはコンストラクタの中で動的に値を決定する方法です。

C#
using System;

namespace ReadonlySample
{
    class Program
    {
        // 1. 宣言時に初期化するパターン
        private readonly int _defaultTimeout = 30;

        // 2. コンストラクタで初期化するパターン
        private readonly string _connectionString;

        public Program(string connectionString)
        {
            // コンストラクタ内であれば、動的に値を決めて代入できる
            _connectionString = connectionString;
        }

        public void DisplaySettings()
        {
            Console.WriteLine($"Timeout: {_defaultTimeout}");
            Console.WriteLine($"Connection: {_connectionString}");
            
            // 以下のような代入はコンパイルエラーになる
            // _defaultTimeout = 60; 
        }

        static void Main(string[] args)
        {
            Program p = new Program("Server=myServerAddress;Database=myDataBase;");
            p.DisplaySettings();
        }
    }
}
実行結果
Timeout: 30
Connection: Server=myServerAddress;Database=myDataBase;

このように、readonlyインスタンスごとに異なる値を固定値として保持したい場合に非常に便利です。

通常の変数では、プログラムの途中で誤って値を書き換えてしまうリスクがありますが、readonlyを付与しておけばコンパイラがエラーとして検知してくれるため、安全性が担保されます。

readonlyとconstの決定的な違い

C#には値を固定するキーワードとしてconstも存在しますが、これらは性質が全く異なります

最も大きな違いは、「いつ値が決定するか」というタイミングと、その「柔軟性」にあります。

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

const「コンパイル時定数」と呼ばれます。

これはプログラムをビルドした瞬間に値が確定し、バイナリの中に直接埋め込まれることを意味します。

そのため、constには数値や文字列のリテラルしか代入できず、計算式やメソッドの戻り値などは使用できません。

対してreadonly「実行時定数」です。

プログラムが起動し、オブジェクトが生成されるタイミングで値が決まります。

設定ファイルから読み込んだ値や、その時の時刻などを元に値を固定することが可能です。

比較一覧表

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

特徴constreadonly
決定タイミングコンパイル時実行時(インスタンス化の際)
代入可能な場所宣言時のみ宣言時、またはコンストラクタ内
静的/インスタンス常にstatic(暗黙的)staticもインスタンスも可能
型の制限組み込み型(int, string等)のみ任意の型(クラス、構造体)が可能
バージョニング問題参照元も再ビルドが必要参照元の再ビルドは不要

バージョニング問題は特に注意が必要です。

constを使用しているライブラリの値を変更した場合、そのライブラリを参照している他のプログラムも再ビルドしない限り、古い値が残ったままになってしまいます。

一方、readonlyであれば実行時に値を参照するため、ライブラリのDLLを差し替えるだけで新しい値が反映されます。

実践的なreadonlyの活用シーン

readonlyは単に「変数を変えない」だけでなく、設計レベルでプログラムの品質を高めるために多用されます。

ここでは代表的な活用例を紹介します。

依存性の注入(DI)における活用

現代的なC#開発(特にASP.NET Coreなど)では、依存性の注入(Dependency Injection)が標準的に使われます。

DIによって注入されたサービス(インスタンス)は、クラス内で変更されるべきではないため、readonlyとして保持するのが鉄則です。

C#
public class OrderService
{
    // DIされるサービスはreadonlyで宣言し、書き換えを防止する
    private readonly IRepository _repository;
    private readonly ILogger _logger;

    public OrderService(IRepository repository, ILogger logger)
    {
        // コンストラクタで受け取ったインスタンスを代入
        _repository = repository;
        _logger = logger;
    }

    public void ProcessOrder(int orderId)
    {
        _logger.LogInfo($"Processing order: {orderId}");
        var order = _repository.GetById(orderId);
        // ... 処理 ...
    }
}

このように設計することで、メソッドの途中で誤って_repository = null;のように書き換えてしまうミスを物理的に防ぐことができます。

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

C# 7.2から導入されたreadonly structは、構造体自体を不変にすることを宣言する機能です。

これにより、コンパイラは「この構造体は絶対に中身が変わらない」と確信できるため、防御的コピー(Defensive Copy)を避ける最適化が可能になり、パフォーマンスが向上します。

C#
// 構造体全体をreadonlyにする
public readonly struct Point
{
    public double X { get; }
    public double Y { get; }

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

    public double CalculateMagnitude() => Math.Sqrt(X * X + Y * Y);
}

readonly structを定義する場合、すべてのフィールドやプロパティも読み取り専用である必要があります。

これは、大規模な数値計算やゲーム開発など、大量のデータを扱う際に非常に有効な手法です。

注意が必要な「参照型」のreadonly

初心者が最も陥りやすい罠が、「readonlyにしても中身が変更できる場合がある」という点です。

これは、対象の変数が「値型」か「参照型」かによって挙動が異なるためです。

参照の固定と中身の変更

readonlyが保証するのは、あくまで「その変数に格納されているアドレス(参照先)が変わらないこと」です。

参照先のオブジェクトが持っているプロパティやリストの中身までは保護しません。

C#
using System;
using System.Collections.Generic;

public class MyClass
{
    // readonlyで宣言されたリスト
    public readonly List<string> Tags = new List<string>();
}

class Program
{
    static void Main()
    {
        var myObj = new MyClass();

        // これはOK(リストの中身を操作している)
        myObj.Tags.Add("C#");
        myObj.Tags.Add("readonly");

        // これはNG(リストそのものを新しいインスタンスに差し替えようとしている)
        // myObj.Tags = new List<string>(); // コンパイルエラー

        Console.WriteLine($"Count: {myObj.Tags.Count}");
    }
}
実行結果
Count: 2

コレクションの中身も保護する方法

もし、リストの内容も含めて完全に読み取り専用にしたい場合は、ReadOnlyCollection<T>や、ImmutableList<T>などを使用する必要があります。

C#
using System.Collections.ObjectModel;
using System.Collections.Generic;

public class SecureClass
{
    private readonly List<string> _internalList = new List<string>();

    // 外部には読み取り専用のビューだけを公開する
    public ReadOnlyCollection<string> Tags { get; }

    public SecureClass()
    {
        _internalList.Add("Initial Item");
        Tags = new ReadOnlyCollection<string>(_internalList);
    }
}

このように、readonlyキーワードと適切なコレクション型を組み合わせることで、真の不変性を実現できます。

最新のC#におけるreadonlyの進化

C#はバージョンアップを重ねるごとに、不変性をより簡単に、かつ安全に扱うための機能を追加してきました。

initのみのプロパティ (C# 9.0〜)

readonlyフィールドの弱点は、コンストラクタ以外で初期化できない点でした。

これを解決するのがinitセッターです。

オブジェクト初期化子を使いつつ、一度設定した後は書き換えを禁止することができます。

C#
public class User
{
    // initを使えば、オブジェクト作成時のみ値をセットできる
    public int Id { get; init; }
    public string Name { get; init; }
}

// 使い方
var user = new User { Id = 1, Name = "Alice" };

// 以下はエラーになる
// user.Id = 2;

initプロパティは内部的にreadonlyフィールドに近い挙動をしますが、外部からの初期化を許容するため、より柔軟なプログラミングが可能になります。

readonlyメンバ (C# 8.0〜)

構造体において、特定のメソッドが状態を変更しないことを明示するreadonly修飾子も導入されました。

構造体全体をreadonlyにするほどではないが、特定の計算メソッドだけは安全に呼び出したい、という場合に有効です。

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

    // このメソッドはフィールドを書き換えないことを保証する
    public readonly double Area => Width * Height;
}

この修飾子を付けることで、コンパイラは不必要なコピーを作成せず、実行効率の高いコードを生成します。

まとめ

readonlyは、C#において「データの安全な管理」「意図の明確化」を両立させるための不可欠な道具です。

単に定数を作るためのconstとは異なり、実行時の柔軟性を保ちながら、一度決めた値を保護する力を持っています。

最後に、活用のポイントを振り返りましょう。

値の固定

プログラム全体で変わらないリテラルにはconstを、インスタンスごとに動的に決まる不変値にはreadonlyを選択する。

参照型の注意

参照型にreadonlyを付けても、そのプロパティやリストの中身は変更可能であるため、必要に応じて不変コレクションを併用する。

設計の基本

DIで注入するフィールドや、再代入の必要がない変数は、まずreadonlyにできないか検討する。

最新機能

initプロパティやreadonly structを活用し、最新のC#らしい安全で高速なコードを目指す。

不変性を意識したコーディングは、最初は手間に感じるかもしれませんが、プロジェクトが大規模になるほどその恩恵は大きくなります。

ぜひ、明日からの開発でreadonlyを積極的に活用してみてください。

変数・データ型・演算子

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!