集合表达式
本教程共 100 篇 · 第 26 篇 · 更新于 2026-07-31 · 约 8 分钟阅读
26. 集合表达式
本节目标:用 C# 12 的集合表达式
[...]一行创建数组、List、Span,并理解空集合与展开元素的用法。
为什么要发明集合表达式
在没有集合表达式(collection expression)之前,创建集合的写法很分裂:数组要用 new int[] { 1, 2, 3 },List<T> 要用 new List<int> { 1, 2, 3 },Span<T> 几乎没有方便的写法。同一种”给我一组数”的意图,却要记三种不同语法,重构时想在数组和列表之间切换也麻烦。
C# 12 引入集合表达式,用一对方括号 [...] 统一表达”一组元素”,具体变成哪种集合,由编译器根据上下文(目标类型)决定。一处写法,多种类型通吃,这就是它最大的价值。
Note集合表达式不是一种新的集合类型,而是一个”目标类型(target-typed)“的语法糖。编译器看你要把它赋给
int[]还是List<int>,就生成对应的底层代码。你写的是同一行,编译器负责适配。
基础写法
方括号里直接列元素,右边变量的类型决定最终产物:
namespace CSharpDemo;
int[] numbers = [1, 2, 3];
List<string> names = ["小明", "小红"];
Span<int> digits = [0, 1, 2];
Console.WriteLine(numbers.Length); // 3
Console.WriteLine(names[0]); // 小明
Console.WriteLine(digits[2]); // 2
把代码读一遍:numbers 是数组,names 是列表,digits 是 Span。三行写法几乎一致,差别只在左边声明的类型。
能用在哪些类型上
从 C# 12 起,集合表达式支持的类型包括:
- 数组(array):
int[]、string[]、Person[]等 Span<T>与ReadOnlySpan<T>- 实现了集合初始化器的类型,最常见的是
List<T> - 还有
IEnumerable<T>、ImmutableArray<T>、HashSet<T>、Stack<T>、Queue<T>等满足构造约定的类型
namespace CSharpDemo;
Span<char> chars = ['a', 'b', 'c'];
List<int> nums = [10, 20, 30];
HashSet<int> unique = [1, 2, 2, 3]; // 自动去重成 1,2,3
Console.WriteLine(unique.Count); // 3
Tip把集合表达式赋给接口类型(如
IEnumerable<int> many = [1, 2, 3];)时,编译器会挑一个合适的实现(通常是数组)来存放。需要具体行为时,显式声明成List<int>更清晰、更可控。
空集合 []
一对空方括号表示”空的集合”,同样能赋给各种类型。它比 new List<string>() 更短,也比数组的 Array.Empty<string>() 更直观。
namespace CSharpDemo;
List<string> emptyList = [];
int[] emptyArray = [];
Console.WriteLine(emptyList.Count); // 0
Console.WriteLine(emptyArray.Length); // 0
Note
[]不是null,它是一个真实存在、元素个数为 0 的空集合。所以你拿它去遍历、取Count都不会空引用异常。这点和返回null的旧习惯不同,更安全,调用方往往可以省掉判空。
展开元素 ..
集合表达式里可以用 ..(两个点)把另一个集合”铺平”塞进来,官方叫 spread element(展开元素)。它像把一段集合拆开、逐个放入结果,而不是把”整个集合”当成一个元素塞进去。
namespace CSharpDemo;
int[] part1 = [1, 2, 3];
int[] part2 = [4, 5];
int[] combined = [.. part1, .. part2];
Console.WriteLine(string.Join(" ", combined)); // 1 2 3 4 5
.. part1 把 part1 里的每个元素依次放进 combined,不是把”整个数组”当成一个元素。
Warning
..后面必须跟一个可枚举的集合(数组、List<T>等)。写成.. 5这种”展开一个单独的数”会编译报错——展开的是集合,不是单个标量值。
混合与嵌套
展开元素可以和普通元素混写,也能拼出交错数组(数组的数组):
namespace CSharpDemo;
int[] part1 = [1, 2, 3];
int[] part2 = [4, 5];
int[] baseNums = [2, 3];
int[] mixed = [1, .. baseNums, 4]; // 1 2 3 4
int[][] grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
Console.WriteLine(grid[1][2]); // 6
int[] extended = [.. mixed, .. part2]; // 1 2 3 4 4 5
Console.WriteLine(extended.Length); // 6
要注意:.. part1 是复制元素,修改 combined 不会影响原来的 part1,两者是独立的。上面在开头声明了 part1/part2,方便示例自包含可运行。
Tip想把几个小数组合并成大数组,以前要写循环或
Concat。现在一行[.. a, .. b, .. c]就搞定,可读性高很多,也更容易一眼看清最终内容。
直接当参数和返回值
集合表达式不只是赋值好用,还能直接作为方法参数或返回值,省掉临时变量:
namespace CSharpDemo;
int Sum(IEnumerable<int> values)
{
int total = 0;
foreach (var v in values) total += v;
return total;
}
Console.WriteLine(Sum([1, 2, 3, 4])); // 10
int[] make() => [9, 8, 7];
Console.WriteLine(make()[0]); // 9
和集合初始化器怎么选
老代码的 new List<int> { 1, 2, 3 }(集合初始化器,collection initializer)依然完全有效,两种写法可以共存。但集合表达式明显更统一:数组、List、Span 用同一套方括号语法,将来在数组和 List 之间切换,只需改左边类型,不用动右边内容。
// 老:new List<int> { 1, 2, 3 }
// 新:List<int> nums = [1, 2, 3];
Note集合表达式从 C# 12 开始可用。本教程基线为 C# 14 / .NET 10,大可放心使用,不必担心语言版本过低报错。它属于”语法糖”,运行效率与对应老写法基本一致,主要收益在简洁和一致。
常见坑
- 展开的不是单个值:
..后面必须是集合,别写成展开标量。 - 它是副本不是视图:
[.. a]复制了a的元素,之后改原数组不影响新集合,反之亦然。 - 目标类型要明确:写
var x = [1, 2, 3];时,编译器会把x推断成int[]。想要List<int>就显式声明类型。 - 别误以为能改:集合表达式造出的是普通集合,之后该
Add就Add、该赋值索引就赋值,它只是”创建”阶段的简写。
最佳实践
- 创建集合优先用
[...],少写new List<T> { }。 - 合并小集合用
[.. a, .. b],比手写了循环清爽。 - 需要空集合就写
[],比返回null更安全,调用方不用判空。 - 当参数或返回值时,按真实需要声明成数组还是
List<T>,别图省事一律用IEnumerable<T>而造成不必要的分配。
集合表达式与可读性
写上 [] 之后,代码的”意图”比”语法”更突出。读者一眼看到的是”这是一组元素”,而不是”在调构造函数、加初始化器”。在团队代码评审里,这种少噪音的写法更容易扫读,新人也能更快看懂。
另外,集合表达式让”数组还是列表”变成了一个可以在声明处轻松切换的细节。过去把某字段从 int[] 改成 List<int>,每个 new int[]{...} 都得跟着改;现在只需改左侧类型,初始化处的 [1,2,3] 一字不动。
Tip当一份数据在多处被当作集合传递时,统一用集合表达式初始化,将来在数组和
List<T>之间迁移几乎零成本。
一个容易混淆的点
有人会问:集合表达式是”数组语法”吗?不是。它只是长得很像数组字面量,最终类型完全由左边决定。把它理解成”一个会被编译器翻译成正确集合的速记法”最准确。
Note因为类型由目标决定,把集合表达式赋给
var时,编译器会选择数组作为默认实现。所以显式写出左侧类型,往往比var更清楚表达意图。开发中不必纠结,记住”左侧类型说了算”即可。
小结
集合表达式用 [...] 一种语法通吃数组、List、Span,配 .. 展开元素能合并、嵌套,空集合用 []。它是 C# 12 起最实用的语法糖之一,让”造一组东西”这件事变得又短又一致。下一章我们换个角度,看字符串本身如何被当成一组字符来操作。