c# 13集合表达式是替代传统初始化的主力机制,优先用于静态、尺寸确定场景;编译器依目标类型生成最优实现(如immutablearray或数组),支持嵌套、展开(..)及编译期常量折叠,但不支持icollection等可变接口。

集合字面量(Collection Expressions)在 C# 12 中正式落地,C# 13 进一步强化了嵌套、展开和不可变语义。它不是语法糖的“锦上添花”,而是替代 new int[] { }、new List<t> { }</t> 和 Enumerable.Range().ToArray() 的主力初始化机制——只要目标类型明确且支持集合初始化,就该优先用 [...]。
什么时候该用 [1, 2, 3] 而不是 new int[] { 1, 2, 3 }
当你要构造一个**静态、一次性、尺寸确定**的集合时,[1, 2, 3] 是默认选择。编译器会直接生成紧凑的 IL(比如 ldc.i4.3 + newarr),不经过任何中间对象或虚方法调用。
- 赋值给
int[]、string[]、Span<char></char>、IReadOnlyList<t></t>等接口时,行为由目标类型决定:写成IReadOnlyList<int> x = [1, 2, 3];</int>实际生成的是ImmutableArray<int></int>,而非托管数组 - 写
var x = [1, 2, 3];时,x类型固定为int[],不会退化为object[]或引发装箱 - 旧写法
new int[] { 1, 2, 3 }在调试器中显示为“数组”,而[1, 2, 3]在 IDE 悬停时直接显示元素和长度,更利于快速验证 - 别在循环体内反复写
[i, i+1, i+2]——每次都会分配新数组;这种动态场景仍该用List<t>.Add()</t>或预分配数组
.. 展开运算符实际怎么工作的
.. 不是运行时反射或 LINQ 遍历,而是编译器在 AST 阶段识别后,生成针对具体类型的高效拷贝逻辑。例如 [..a, ..b] 赋值给 int[],会被重写为 Array.Copy 序列;若目标是 Span<int></int>,则用 Span<t>.CopyTo</t>。
- 被展开的对象必须实现
IEnumerable<t></t>,且编译器能推导出T;HashSet<int></int>虽然可枚举,但不支持集合表达式直接展开(会报错 CS8955) -
..可多次出现:[x, ..list, y, ..span, z]合法,顺序严格按书写顺序拼接 - 不能对
null展开:[..possiblyNullList]编译通过,但运行时抛NullReferenceException;需显式判空或用空合并:[..possiblyNullList ?? []] - 嵌套展开不自动扁平化:
var jagged = [[1, 2], [3, 4]];类型是int[][],不是int[];要扁平化得写[..jagged[0], ..jagged[1]]
为什么 IReadOnlyCollection<t></t> 推荐但 ICollection<t></t> 不行
集合表达式设计哲学明确倾向不可变/只读契约。IReadOnlyCollection<t></t>、IReadOnlyList<t></t>、IEnumerable<t></t> 都被原生支持,而 ICollection<t></t> 和 IList<t></t> 不在隐式转换列表中——因为它们带 Add、Remove 等可变方法,与字面量“声明即完成”的语义冲突。
- 写
ICollection<int> c = [1, 2, 3];</int>会编译失败(CS0029);必须显式构造:new List<int> { 1, 2, 3 }</int> -
List<t></t>被支持,是因为它实现了集合初始化器(Add方法),但底层仍是先分配数组再逐个Add,性能不如直接生成数组;仅当后续需修改时才选它 - 若你定义自定义集合类型,要让它支持
[...],必须公开无参构造函数 +Add(T)方法,或实现ISpanFormattable等特定接口(C# 13 新增)
最易被忽略的一点:集合表达式中的元素表达式会在**编译期求值**(如常量折叠),但变量引用和方法调用(如 [1, ..GetItems(), 2])仍为运行时行为。别假设整个 [...] 块是“编译时常量”——只有纯字面量部分才是。











