C# 冻结对象Frozen Objects方法 C#如何创建真正不可变的集合以提升性能

来源:这里教程网 时间:2026-02-21 17:40:38 作者:

为什么
FrozenSet
FrozenDictionary
不是 .NET 原生类型

.NET 没有内置的

FrozenSet
FrozenDictionary
类型——这是常见误解的源头。所谓“冻结对象”,在 C# 中实际指**构建后不可修改、且内部结构被优化为只读访问的集合**。真正的不可变集合来自
System.Collections.Immutable
包,但要注意:它的
ImmutableArray<t></t>
ImmutableList<t></t>
等仍是“不可变”(即每次修改返回新实例),而非“冻结”(即内存布局固定、无写时复制开销)。

真正接近“冻结”语义的是

ImmutableArray<t>.AsReadOnly()</t>
返回的
ReadOnlyArray<t></t>
,或更直接地使用
System.Runtime.CompilerServices.IsExternalInit
配合
init
属性 +
readonly struct
手动建模;但若目标是高性能只读集合访问(如配置缓存、枚举映射表),应优先考虑
ImmutableArray<t></t>
的预构建 +
AsFrozen()
模式(需手动实现)或转向
Span<t></t>
/
Memory<t></t>
驱动的只读视图。

ImmutableArray<t>.Builder</t>
构建一次性不可变数组

这是最常用、也最贴近“冻结”效果的做法:先用可变 builder 填充数据,再调用

ToImmutable()
得到不可变快照。它不支持后续修改,且底层是紧凑数组,无链表/树结构开销,访问性能等同原生数组。

ImmutableArray.CreateBuilder<int>()</int>
创建 builder,支持
Add
Insert
Clear
等操作
填充完成后调用
builder.ToImmutable()
→ 返回
ImmutableArray<int></int>
,其
IsDefault
Length
访问都是 O(1),索引访问无装箱、无虚调用
避免在循环中反复调用
builder.Add(x)
后又立即
ToImmutable()
—— 这会触发多次数组拷贝;应一次性填完再冻结
若已知大小,用
ImmutableArray.CreateBuilder<int>(capacity)</int>
预分配,减少扩容拷贝

如何让
ImmutableList<t></t>
ImmutableHashSet<t></t>
“真正冻结”

ImmutableList<t></t>
底层是平衡树,
ImmutableHashSet<t></t>
是哈希 trie,二者都为高效更新设计,但代价是内存碎片和访问间接跳转。若集合构建后永不修改,它们就“过重”了。

此时应转换为更轻量的只读形态:

对键值对场景,用
ImmutableArray<keyvaluepair tvalue>>.ToImmutable()</keyvaluepair>
+ 自定义只读包装器,比
ImmutableDictionary
节省内存 40%+(实测 10k 条目)
对去重集合,用
ImmutableHashSet<t>.ToImmutableArray()</t>
再转
ImmutableArray<t></t>
,然后用
Array.BinarySearch
(需先排序)或
HashSet<t>.Contains</t>
初始化一个临时
HashSet<t></t>
作查找加速 —— 注意这不是“冻结”,但能模拟冻结后的 O(1) 查找
不要依赖
AsReadOnly()
方法返回的
IReadOnlyList<t></t>
接口:它只是编译期防护,运行时仍可能被反射或强制转型绕过;真正安全靠的是类型本身不可变(如
ImmutableArray<t></t>

冻结集合的陷阱:引用类型字段仍可变

即使你用了

ImmutableArray<person></person>
,如果
Person
是 class,其内部字段仍可被修改 —— “冻结”只作用于集合容器本身,不递归冻结元素。

若需深度冻结,元素类型必须是
readonly struct
,或使用
record class
(注意:record class 默认可变字段仍可改,要用
init
属性 +
private set
封装)
常见错误:
var frozen = ImmutableArray.Create(new Person { Name = "A" });
→ 后续
frozen[0].Name = "B"
依然合法(如果
Name
是 public set)
验证方式:对元素类型启用
[ImmutableObject(true)]
并配合静态分析器(如 Microsoft.CodeAnalysis.FxCopAnalyzers),但该特性仅作提示,不强制执行
最稳妥路径:用
record struct
(C# 10+)定义元素,天然只读且栈语义,与
ImmutableArray
组合时零堆分配、零 GC 压力

真正冻结的关键不在“不可变接口”,而在**数据生命周期与内存布局的确定性**:builder 一次性构建、struct 元素杜绝副作用、避免泛型集合的装箱与虚方法分发。这些点稍不注意,所谓的“冻结”就只剩心理安慰。

相关推荐