C# 在集合初始化这件事上,过去一直存在写法分散的问题:数组、List<T>、Span<T> 甚至自定义集合,语法各不相同。到了 C# 12.0 与 .NET 8.0,集合表达式用统一的 [...] 语法把这些场景收拢起来。本文会从它解决的问题、核心语法、支持范围和适用边界几个角度展开,帮助你快速看懂这项新特性值不值得在项目里采用。
集合表达式解决了什么问题
集合表达式(Collection Expressions)是 2023 年随 .NET 8.0 发布、由 C# 12.0 引入的新特性。它的目标很直接:用更短、更统一、更接近声明式的方式创建和初始化集合。
在旧写法里,不同集合往往要记不同模板:
- 数组常写成
new[] { ... }或new int[] { ... } List<T>常写成new List<T> { ... }- 部分场景还要考虑目标类型推断、空集合写法和嵌套结构
集合表达式统一改成 [...] 之后,代码会更集中,阅读时也更容易一眼看出“这是一个集合值”,而不是被不同的构造形式打断。
这项能力尤其适合下面几类场景:
- 数据初始化:快速定义数组、列表、字典等集合
- 批量操作:例如配合
MySqlBulkLoader处理批量数据填充 API输入/输出:组织JSON响应或请求中的集合数据- 缓存管理:初始化
MemoryCache的键值集合 - 定时任务:结合
Cronos组织批处理数据
核心语法怎么写
集合表达式的基本形式只有一个核心:[element1, element2, ..., elementN]。它看起来很轻,但背后依然会根据目标类型转换成对应集合。

传统初始化与新写法对比
// 传统方式
List<int> list1 = new List<int> { 1, 2, 3 };
int[] array1 = new int[] { 1, 2, 3 };
// C# 12 集合表达式
List<int> list2 = [1, 2, 3];
int[] array2 = [1, 2, 3];
Span<int> span = [1, 2, 3];
从可读性看,最明显的变化是去掉了重复出现的 new、类型名和花括号模板。尤其在方法参数、返回值或者嵌套结构里,简化效果会更明显。
几种最常用的写法
基础初始化:
int[] numbers = [1, 2, 3]; // 等价于 new[] { 1, 2, 3 }
List<string> names = ["Alice", "Bob"]; // 等价于 new List<string> { "Alice", "Bob" }
空集合:
int[] empty = []; // 等价于 Array.Empty<int>()
嵌套集合:
int[][] jagged = [[1, 2], [3, 4]]; // 锯齿状数组
另一种常见的嵌套形式如下:
// 嵌套集合
int[][] matrix = [[1, 2], [3, 4], [5, 6]];
扩展运算符与类型推断
集合表达式不只是“少写几个字”,它还支持通过 .. 展开已有集合,这在合并数组或列表时很实用。
int[] moreNumbers = [1, 2, ..numbers, 4]; // 展开 numbers,生成 [1, 2, 1, 2, 3, 4]
它也支持目标类型推断,也就是编译器根据上下文决定最终集合类型:
IEnumerable<int> enumerable = [1, 2, 3]; // 推断为数组
这意味着你在接口类型、方法参数和返回值位置使用集合表达式时,通常可以少写一层显式构造代码。不过也正因为依赖目标类型,阅读时最好确认上下文,避免把“看起来像列表”的表达式误解成固定实现类型。
它支持哪些集合类型
这项特性不是只给数组和 List<T> 用的。根据给定上下文,编译器可以把集合表达式转换到多种目标类型。

| 集合类型 | 示例 | 编译器转换机制 |
|---|---|---|
| 数组 | int[] arr = [1, 2, 3]; | 直接优化为 new int[] { ... } |
| List | List list = [1, 2, 3]; | 调用 List.CollectionsMarshalHelper.Create() |
| Span | Span span = [1, 2, 3]; | 栈分配或缓冲区重用 |
| ReadOnlySpan | ReadOnlySpan ros = ['a','b']; | 同 Span |
| ImmutableArray | ImmutableArray ia = [1,2,3]; | 调用 ImmutableArray.Create() |
| 自定义集合 | 实现 IEnumerable + Add 方法 | 编译器生成 Add 调用序列 |
✅ 所有实现 IEnumerable 且有 Add 方法的类型都支持
从使用角度看,可以把支持范围分成三类来理解:
内建目标类型
- 数组
List<T>Span<T>ReadOnlySpan<T>ImmutableArray
这些类型一般都有较明确的编译器处理路径,因此既保留了简洁语法,也兼顾了性能优化空间。
接口类型与兼容性
集合表达式也能对接常见抽象接口,例如:
IEnumerable<T>ICollection<T>
这让它非常适合写在方法返回值、配置对象初始化和中间结果拼装上。代码层面仍然是一个集合值,但调用端不需要关注底层具体用的是哪种实现。
自定义集合怎么接入
如果你有自己的集合类型,只要满足两个基本条件,也可以直接吃到这套语法:
- 实现
IEnumerable - 提供
Add方法
例如:
public class CustomCollection<T> : IEnumerable<T>
{
private readonly List<T> _items = new();
public void Add(T item) => _items.Add(item);
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
}
// 使用集合表达式初始化
CustomCollection<string> tags = ["dotnet", "csharp"];
这里可以把它理解成:编译器看到目标类型具备集合初始化所需能力,就会为你生成相应的 Add 调用序列。
除了基础初始化,还有哪些实用形式
原始介绍里提到的几个能力,真正放到项目里时,最值得关注的是下面四种:
1. 统一而简洁的写法
- 使用
[...]替代new List<T> { ... }或new[] { ... } - 同一套写法可覆盖数组、
List<T>、Span<T>等目标类型
这对于频繁写 DTO、配置项、测试数据和示例代码的人尤其友好,能明显减少模板噪音。
2. 用 .. 组合已有集合
扩展运算符适合把已有数据拼接进新集合。相比先建一个空列表再逐步 AddRange,它更接近“声明结果长什么样”。在返回结果集、构造参数列表和拼接默认值时会很好用。
3. 不只用于声明变量
集合表达式可以用于初始化集合、传递参数、返回结果,也能出现在和 LINQ、异步操作、并发逻辑相邻的代码中。它本身不是这些能力的替代品,但能让周边集合构造更紧凑。
4. 字典场景也能覆盖
除了顺序集合,文中还给出了字典初始化写法:
Dictionary<string, int> scores = ["Alice": 90, "Bob": 85]; // 等价于 new Dictionary<string, int> { ["Alice"] = 90, ["Bob"] = 85 }
这让“数组、列表、字典分别记三套初始化模板”的情况进一步减少,统一感会更强。
什么时候值得用,什么时候先别急着全量替换
如果你的项目已经升级到 .NET 8.0 并使用 C# 12.0,集合表达式很适合作为默认的新代码写法,尤其是以下情况:

- 需要频繁初始化小型集合
- 示例代码、测试代码、配置代码较多
- 希望数组、列表、接口类型初始化风格统一
- 项目中已经大量使用目标类型推断
但在这些情况下,最好先局部采用,再考虑逐步推广:
- 团队还不熟悉
C# 12新语法 - 代码库需要同时兼容较旧语言版本
- 某些位置过度依赖上下文推断,可能降低一眼可读性
一个比较稳妥的判断标准是:如果 [...] 让代码更短且不牺牲理解成本,就值得用;如果必须停下来推断它最终到底是数组、列表还是别的实现类型,那就应该适当保留显式类型信息。
整体来看,集合表达式并不是改变集合能力本身,而是把“创建集合”这件事统一成了更现代的语法入口。对日常业务开发来说,它最大的价值不是炫技,而是减少样板代码、提升一致性,并让集合初始化更接近代码真正想表达的数据结构。









