immutablelist不可变性要求所有修改操作必须接收返回值,否则逻辑失效;其树形结构带来访问开销,高频索引场景应优先选immutablearray。

直接用 ImmutableList<t></t> 替代 List<t></t> 做只读场景,基本等于给自己埋雷——它不是“只读包装”,而是带结构共享、值语义明确的树形集合,必须按不可变范式使用,否则性能暴跌或逻辑静默失效。
为什么 ImmutableList.Add() 不接返回值就等于没写
因为所有修改方法(Add()、Remove()、SetItem())都不改变原实例,只返回新实例。原变量仍指向旧树节点,新增/删除内容彻底丢失。
-
list.Add(42);→ 编译通过,但无实际效果,list仍是旧引用 -
var newList = list.Add(42);→ 正确,newList指向新构建的平衡树节点 - 链式调用可行:
list.Add(1).Remove(0).Insert(0, 99),但每步都新建中间对象,100 次循环会生成 100 个临时树,GC 压力陡增
ImmutableList.Empty 和 ImmutableList.Create() 的开销差异
两者都创建空实例,但底层构造和适用场景完全不同:
-
ImmutableList<int>.Empty</int>是静态只读单例,零分配、零装箱,适合纯占位或作为Builder起点 -
ImmutableList.Create<int>()</int>返回一个含 0 个元素的树节点,有构造开销和装箱成本,适合后续链式调用(如.Add(1).Add(2)) - 误用
new List<int>().AsReadOnly().ToImmutableList()</int>:多一层ReadOnlyCollection<t></t>包装,白跑一次遍历,还引入冗余引用
Builder 模式什么时候必须用
批量变更(>10 次增删改)或循环中累积操作时,不用 Builder 就是自找麻烦:
-
var builder = list.ToBuilder();开销极小,底层是可变数组,非树结构 -
builder.Add()、builder.RemoveAt()、builder[0] = x全部是O(1)操作 -
builder.ToImmutable()才真正构建平衡树,仅一次内存分配 - 已知容量?用
ImmutableList.CreateBuilder<t>(capacity)</t>预分配,避免内部数组扩容拷贝
ImmutableList vs ImmutableArray:高频只读访问怎么选
不是越“不可变”越好,要看访问模式:
- 需要频繁索引访问(如配置表查值)、且构建后永不修改 → 选
ImmutableArray<t></t>:底层是紧凑数组,array[i]是纯指针偏移,无虚调用、无装箱、无树跳转 - 需要后续仍可能小范围增删(如事件队列、状态快照链)→ 选
ImmutableList<t></t>:树结构支持高效插入/删除,但每次访问要多跳 1–2 层指针 - 误把
ImmutableList<t></t>当“高性能只读数组”用:访问延迟比ImmutableArray<t></t>高 3–5 倍,尤其在热路径上明显
真正容易被忽略的是:ImmutableList 的“不可变”不等于“零开销”。它用结构共享换线程安全,但树形访问和中间对象分配在高频、低延迟场景下会立刻暴露代价。










