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>型は、Add、Remove、Clear、Insertといった、コレクションを変更するメソッドを一切持ちません。[i]によるインデックスアクセス(読み取り)やCount、foreachによる列挙は可能です。
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なプロパティとして安全に公開し、カプセル化を維持するために非常に有効な手段です。
