【C#】List.AsReadOnlyメソッドで読み取り専用のビュー(Wrapper)を作成する方法

目次

List<T>を読み取り専用として公開したい

System.Collections.Generic.List<T>は、Add, Remove, Insertなどのメソッドを持ち、要素を自由に変更(追加・削除)できる「可変(Mutable)」なコレクションです。

しかし、クラスの内部で管理しているList<T>を、メソッドの戻り値やpublicなプロパティとして外部に公開する場合、List<T>のまま返してしまうと、そのクラスの外部(呼び出し元)から意図せずリストの中身を変更されてしまう(例: myClass.MyList.Add(...))可能性があります。

このような「内部のリストは変更させたくないが、中身は参照(読み取り)させたい」というカプセル化の要求に応えるのが、List<T>.AsReadOnly()メソッドです。


AsReadOnly() メソッドの基本的な使い方

List<T>.AsReadOnly()メソッドは、元のList<T>インスタンスの「読み取り専用ラッパー」を返します。

  • 戻り値の型: System.Collections.ObjectModel.ReadOnlyCollection<T>
  • 動作: ReadOnlyCollection<T>型は、AddRemoveClearInsertといった、コレクションを変更するメソッドを一切持ちません。[i]によるインデックスアクセス(読み取り)やCountforeachによる列挙は可能です。

C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; // ReadOnlyCollection<T> のため

public class AsReadOnlyBasicExample
{
    public static void Main()
    {
        // 元のリスト
        var tasks = new List<string> { "Task 1", "Task 2" };

        // 読み取り専用ラッパーを作成
        ReadOnlyCollection<string> readOnlyTasks = tasks.AsReadOnly();

        // 読み取り操作 (Count, インデックス[0], foreach) は可能
        Console.WriteLine($"要素数: {readOnlyTasks.Count}");
        Console.WriteLine($"最初の要素: {readOnlyTasks[0]}");

        // 変更操作はコンパイルエラーになる
        // readOnlyTasks.Add("Task 3"); // エラー: 'Add' メソッドが存在しない
        // readOnlyTasks[0] = "New Task"; // エラー: インデクサは読み取り専用
    }
}

重要な特性:コピーではなく「ラッパー」

AsReadOnly()を使用する上で最も重要な点は、このメソッドがList<T>コピー(スナップショット)を作成するわけではない、ということです。

ReadOnlyCollection<T>は、元のList<T>インスタンスを内部で参照し続ける「ラッパー(Wrapper)」または「ビュー(View)」として機能します。

このため、元のList<T>インスタンスが変更されると、その変更はReadOnlyCollection<T>ビューにも即座に反映されます

コード例:元のリストの変更が反映される

C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

public class WrapperBehaviorExample
{
    public static void Main()
    {
        // 1. 元のリストを作成
        var projectFiles = new List<string>
        {
            "main.js",
            "style.css"
        };

        // 2. 読み取り専用ラッパーを作成 (この時点では 2 要素)
        ReadOnlyCollection<string> readOnlyFiles = projectFiles.AsReadOnly();

        Console.WriteLine("--- ラッパー作成直後 ---");
        PrintCollection(readOnlyFiles);

        // 3. 「元のリスト」に要素を追加
        Console.WriteLine("\n... 元のリスト (projectFiles) に 'utils.js' を追加 ...");
        projectFiles.Add("utils.js");

        // 4. 「読み取り専用ラッパー」を参照すると、変更が反映されている
        Console.WriteLine("\n--- 元のリスト変更後 ---");
        PrintCollection(readOnlyFiles); // 3 要素になっている

        // 5. 「元のリスト」の要素を変更
        Console.WriteLine("\n... 元のリスト (projectFiles) の [0] を変更 ...");
        projectFiles[0] = "MODIFIED_main.js";
        
        Console.WriteLine("\n--- 元のリスト要素変更後 ---");
        PrintCollection(readOnlyFiles); // "MODIFIED_main.js" が反映されている
    }

    private static void PrintCollection(ReadOnlyCollection<string> collection)
    {
        Console.WriteLine($"要素数: {collection.Count}");
        foreach (var item in collection)
        {
            Console.WriteLine($" - {item}");
        }
    }
}

出力結果:

--- ラッパー作成直後 ---
要素数: 2
 - main.js
 - style.css

... 元のリスト (projectFiles) に 'utils.js' を追加 ...

--- 元のリスト変更後 ---
要素数: 3
 - main.js
 - style.css
 - utils.js

... 元のリスト (projectFiles) の [0] を変更 ...

--- 元のリスト要素変更後 ---
要素数: 3
 - MODIFIED_main.js
 - style.css
 - utils.js

AsReadOnlyの主な用途(カプセル化)

この「ラッパー」という特性は、クラスの内部状態を保護する「カプセル化」に最適です。

privateフィールドとしてList<T>(変更可能)を持ち、publicプロパティとしてReadOnlyCollection<T>(読み取り専用)を公開するのが、C#における標準的なデザインパターンです。

C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

// ログ履歴を管理するクラス
public class LogHistory
{
    // 内部では変更可能な List<T> でデータを保持
    private readonly List<string> _logs = new List<string>();

    // 外部へは ReadOnlyCollection<T> として公開
    public readonly ReadOnlyCollection<string> Logs;

    public LogHistory()
    {
        // コンストラクタで、_logs のラッパーを public な Logs プロパティに設定
        this.Logs = this._logs.AsReadOnly();
    }

    // クラス内部からは _logs を変更できる
    public void AddLog(string message)
    {
        this._logs.Add($"[{DateTime.Now:HH:mm:ss}] {message}");
    }
}

public class EncapsulationExample
{
    public static void Main()
    {
        var logger = new LogHistory();
        
        // 内部メソッド経由でのみ追加が可能
        logger.AddLog("System initialized.");
        logger.AddLog("User logged in.");

        // 外部からは読み取り専用プロパティ (Logs) を参照
        Console.WriteLine("--- Log History ---");
        foreach (var log in logger.Logs) // 変更が反映されている
        {
            Console.WriteLine(log);
        }

        // 外部から直接変更しようとするとコンパイルエラー
        // logger.Logs.Add("Hacking attempt!"); // コンパイルエラー
    }
}

まとめ

List<T>.AsReadOnly()は、List<T>の「読み取り専用ビュー」であるReadOnlyCollection<T>を返します。

  • コピーではありません: 元のList<T>へのラッパー(Wrapper)です。
  • 反映: 元のList<T>が変更されると、ReadOnlyCollection<T>にも即座に反映されます。
  • 用途: クラスのprivateなリストをpublicなプロパティとして安全に公開し、カプセル化を維持するために非常に有効な手段です。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次