ReadOnlyCollection 是零开销只读包装,但非深只读;IReadOnlyList 是接口,用于契约与泛型约束;ToArray() 拷贝数据,Memory<T> 适用于高性能短生命周期场景。

ReadOnlyCollection 是最常用且安全的只读包装方式
直接用 new ReadOnlyCollection<t>(list)</t> 包装一个已存在的 List<t></t>,这是 .NET Framework 2.0 就支持的方案,运行时开销极小,且能真正阻止修改——调用 Add、Remove 等方法会抛出 NotSupportedException。
注意它不是“深只读”:如果原 List<t></t> 后续被其他代码修改,ReadOnlyCollection<t></t> 的内容也会变(它是引用包装,不是拷贝)。所以确保原始集合不再被意外修改,或者在构造前先做防御性拷贝:
var source = new List<string> { "a", "b" };
var readOnly = new ReadOnlyCollection<string>(new List<string>(source)); // 防御性拷贝
- 适用于需要向外部暴露只读视图,但内部仍需维护可变集合的场景
- 兼容所有 .NET 版本(包括 .NET Framework 4.5+ 和 .NET Core / .NET 5+)
- 不要对
ReadOnlyCollection<t></t>调用AsReadOnly()——那是List<t></t>的扩展方法,返回的仍是ReadOnlyCollection<t></t>,语义重复
IReadOnlyList 多用于接口契约和泛型约束
IReadOnlyList<t></t> 是接口,不能直接 new。它主要用在参数类型、返回类型或泛型约束中,强调“我只要索引访问 + 不修改”的契约。实际返回时,通常还是用 ReadOnlyCollection<t></t> 或数组(T[])来实现它:
public IReadOnlyList<int> GetNumbers() => new ReadOnlyCollection<int>(new List<int> { 1, 2, 3 });
数组也天然实现 IReadOnlyList<t></t>(从 .NET 4.5 开始),所以如果数据固定,直接返回 new[] { 1, 2, 3 } 更轻量。
- 避免在方法内部 new
List<t>().AsReadOnly()</t>后再转成IReadOnlyList<t></t>——这多一次装箱和间接层,不如直接 newReadOnlyCollection<t></t> - 若泛型方法约束为
where T : IReadOnlyList<U>,传入ReadOnlyCollection<u></u>或U[]都合法;但传List<u></u>不行(它不实现该接口) -
IReadOnlyList<t></t>没有Count属性的 setter,但有Countgetter 和this[int]索引器,比IEnumerable<t></t>更适合随机访问场景
别误用 Array.AsReadOnly() ——它不存在
数组没有 AsReadOnly() 方法。常见错误是看到 List<t>.AsReadOnly()</t> 就类推到数组,结果编译失败。数组本身就是只读的(除非你用 Array.Copy 或反射强行改),且已实现 IReadOnlyList<t></t>,不需要额外包装。
如果你写 myArray.AsReadOnly(),编译器会报错:'T[]' does not contain a definition for 'AsReadOnly'。
- 正确做法:直接返回数组,或显式转型
(IReadOnlyList<T>)myArray(通常不需要,协变/隐式转换已覆盖) - 若真需要“不可变数组副本”,用
Array.AsReadOnly()是错的;应改用ReadOnlyCollection<t>(myArray.ToList())</t>,或更高效地用Memory<T>.ToArray().AsReadOnly()(仅限 .NET Core 2.1+)
性能与语义:ReadOnlyCollection vs ToArray() vs Memory
三者都提供只读语义,但成本和适用场景不同:
-
ReadOnlyCollection<t></t>:零分配(仅包装对象),适合长期持有、频繁索引访问,但依赖原始集合生命周期 -
ToArray():分配新数组,内容拷贝,内存开销大,适合一次性返回、避免外部引用干扰 -
Memory<T>(配合ReadOnlyMemory<T>):.NET Core 2.1+ 引入,支持栈分配(stackalloc)、无 GC 压力,适合高性能、短生命周期场景(如解析、序列化中间表示);但它不是集合接口,不能当IReadOnlyList<t></t>直接用,需通过.ToArray()或Span<T>.ToArray()转换
最容易被忽略的一点:ReadOnlyCollection<t></t> 的 Count 和索引器都是 O(1),但它的枚举器(GetEnumerator())会委托给底层列表——如果底层列表是自定义实现且 GetEnumerator() 有副作用或性能问题,这个委托就会暴露出来。










