ImmutableList不同创建方式的差异、性能及适用场景对比
ImmutableList<T> 是.NET提供的不可变列表实现,底层用平衡二叉树存储,不是普通List那种连续数组结构,不同创建方式的内部逻辑差很多,性能和适用场景完全不一样,逐个拆解如下:
各方式实现原理与表现
方式1:先构造普通
List<T>再调用ToImmutableList()List<int> numbers = new List<int>(){0, 1, 2, 3}; ImmutableList<int> immutableList = numbers.ToImmutableList();实现逻辑:
ToImmutableList()本身没有特殊逻辑,内部就是直接调用ImmutableList.CreateRange()把当前List传进去,一次性遍历源数据构造平衡二叉树,不会额外拷贝数据做多余操作。
性能表现:如果你手里本来就有一个填好数据的普通List,这个写法没有额外开销,树构造的效率和CreateRange完全一致;但要是你为了转不可变列表特意先new一个普通List填数据,等于平白多了一次临时List的内存分配和写入,纯浪费性能。
适用场景:仅适合你已经持有现成普通List实例,需要转成不可变列表的场景,不要为了转不可变特意先建普通List。方式2:直接调用
ImmutableList.Create<T>()传入固定个数元素ImmutableList<int> immutableList = ImmutableList.Create<int>(0, 1, 2, 3);实现逻辑:这是params参数重载,传少量元素的时候,内部会直接构造对应的树节点,不需要走批量集合遍历的逻辑;如果传的元素多,params会先生成一个临时数组存所有参数,再走和CreateRange一样的批量构造逻辑。
性能表现:传个位数固定元素的时候性能最高,没有任何多余的临时集合开销,代码也最短;元素多了之后会多一次params数组的分配,性能比CreateRange差一点。
适用场景:创建元素固定、数量少(一般5个以内)的不可变列表,比如固定常量集合、配置选项列表这类场景,写起来最简洁。方式3:直接调用
ImmutableList.CreateRange<T>()传入源集合ImmutableList<int> immutableList = ImmutableList.CreateRange<int>(new List<int>() { 0, 1, 2, 3 });实现逻辑:这是所有批量创建不可变列表的底层统一入口,它会先判断传入的集合有没有已知长度(比如实现了
ICollection<T>就有Count属性),如果有就直接按总长度一次性构造整棵平衡二叉树,避免逐元素添加时反复调整树结构带来的开销;如果传的是没有长度的惰性枚举(比如LINQ查询返回的IEnumerable),才会逐元素遍历构造。如果传入的源本身就是ImmutableList,它会直接返回原实例,连构造都省了。
性能表现:如果你传入的是现成的、带已知长度的集合,这是批量创建性能最高的方式,没有多余的中间开销;但要是你为了调用这个方法特意new一个临时List传进去,那和方式1的多余开销一模一样,没必要。
适用场景:你已经持有现成的任意IEnumerable<T>集合需要转不可变列表时,这是最标准的API,性能最优。方式4:使用Builder模式构造后转ImmutableList
ImmutableList<int>.Builder builder = ImmutableList.CreateBuilder<int>(); builder.AddRange(new List<int>() { 0, 1, 2, 3 }); ImmutableList<int> immutableList = builder.ToImmutableList();实现逻辑:Builder内部维护的是一个普通的可变列表,不是二叉树结构,你在Builder上做Add、Remove、AddRange这类修改操作的时候,都是直接在这个可变列表上改,完全不需要做树平衡调整,等最后调用
ToImmutableList()的时候,才会一次性把内部存的数据构造成平衡二叉树,返回最终的不可变列表实例。
性能表现:如果你构造列表的过程中需要做多次增删改操作,Builder的性能比直接反复操作ImmutableList高几个数量级——毕竟直接改ImmutableList每次修改都会生成一棵新树,开销极大;但如果像示例代码里那样只调用一次AddRange就直接转不可变,性能反而比CreateRange差一点,多了一层Builder内部可变列表的维护开销。
适用场景:构造不可变列表的过程中需要多次动态调整元素,比如循环添加元素、按条件增删元素、拼接多个数据源这类场景,不要在只需要一次性把现成集合转不可变的时候用Builder,属于画蛇添足。
选型总结
- 少量固定元素:直接用
ImmutableList.Create<T>(),简洁高效。 - 已有现成集合需要转换:直接用
ToImmutableList()或者CreateRange<T>(),二者底层逻辑一致,选写着顺手的就行,是批量场景里性能最高的写法。 - 构造过程需要多次动态修改元素:必须用Builder,能避免大量无意义的树构造开销。
- 永远不要为了转不可变列表特意new一个临时普通List,完全是多余的性能浪费。
内容的提问来源于stack exchange,提问作者M.Jaskuski

