在C# 11引入列表模式(List Patterns)之后,针对数组或List等线性集合的结构化匹配变得非常直接。过去我们常常需要写循环、用索引访问或者借助LINQ的Take、Skip来判断集合头部或整体形态,而现在可以在is表达式或switch中直接用方括号语法描述期望的集合内容。这种能力不仅减少了临时变量,也让业务规则一眼可读。

一、列表模式的基本语法
列表模式使用方括号[]来包裹一系列子模式,每个子模式对应集合中从索引0开始的一个元素。最常见的形式包括常量模式、var模式和丢弃模式(下划线)。当被匹配的对象实现了Length或Count以及索引器(如数组、List<T>、Span<T>等),编译器就能生成对应的元素检查逻辑。
例如,我们需要判断一个int数组是不是长度为3,并且第一个元素是1、第二个是2、第三个可以是任意值。传统写法要写多个条件,而列表模式只需一行。下面给出基础示例,注意代码块内所有尖括号都已转义:
int[] numbers = { 1, 2, 99 };
// 列表模式匹配:长度为3,前两个固定,第三个忽略
if (numbers is [1, 2, _])
{
Console.WriteLine("匹配成功:前两位是1和2");
}
// 使用 var 捕获具体值
if (numbers is [1, 2, var third])
{
Console.WriteLine($"第三位是:{third}");
}
上面的_是丢弃模式,表示“这里有任何值都行,但不关心具体内容”。而var third则会把实际元素绑定到变量third上,方便后续使用。这两种写法在编译后都会先检查数组长度是否为3,再依次比对或取值。
二、切片模式捕获剩余元素
如果集合长度不固定,但我们只关心头尾或中间某段,就可以用切片模式..。切片模式代表“零个或多个剩余元素”,并且可以配合var把剩余部分捕获成一个新的集合。切片在列表中只能出现一次,且必须能明确推导出前后元素的位置关系。
假设我们处理用户输入的命令参数,第一个是命令名,后面可能跟任意数量的参数。用切片模式可以轻松拆解:
string[] args = { "run", "--verbose", "file.txt" };
if (args is ["run", .. var rest])
{
Console.WriteLine($"执行run命令,参数个数:{rest.Length}");
foreach (var item in rest)
{
Console.WriteLine(item);
}
}
这里.. var rest把索引1及以后的所有元素捕获为string数组rest。如果args长度仅为1,rest也会是一个空数组,匹配依然成立。切片模式极大简化了“前缀匹配”类需求,不需要手动写Skip(1)。
三、与switch表达式结合做分支分发
列表模式真正发挥威力是在switch表达式中。我们可以根据集合的形状返回不同结果,而不必写层层if-else。下面的例子对一个坐标点数组做分类:
int[] point = { 0, 0 };
string description = point switch
{
[0, 0] => "原点",
[0, var y] => $"Y轴上的点,y={y}",
[var x, 0] => $"X轴上的点,x={x}",
[var x, var y] => $"普通点({x},{y})",
[] => "空集合",
_ => "更多维度的数据"
};
Console.WriteLine(description);
这段代码中,编译器会按照从上到下的顺序尝试匹配。空数组匹配[],二维点根据坐标值落入不同分支。需要注意,如果写_作为兜底,它必须放在最后,否则后面的分支永远无法到达。列表模式与switch结合后,原本需要多个Length判断和索引访问的逻辑被压缩成了一张“形状对照表”。
对于List<T>对象,上述写法完全通用,因为List也提供了Count和索引器。不过若集合是IEnumerable但没有实现索引器,则无法使用列表模式,这是编译期就会报错的限制。
四、常见误用与性能注意点
一个常见误区是认为列表模式会自动递归匹配嵌套集合。实际上[1, [2, 3]]这种写法在C#中并不表示“第二个元素是一个数组[2,3]”,除非内层本来就是模式(如对元素本身再用列表模式需写成[1, var x] where x is [2,3]或用属性模式)。直接嵌套方括号只适用于元素自身支持列表模式的情形,比如元素是数组时可以用[ [1,2], _ ]匹配外层数组的第一个元素是长度为2且值为1、2的数组。
另一个注意点是切片模式不能出现多次,也不能和“长度不固定但又要求尾部固定”之外的复杂约束混用。如果业务需要“中间某段忽略、头尾都固定”,可以写成[head, .., tail],但无法表达“忽略前两个之后的第三个必须是5”这类跳跃约束,那种情况仍需配合where子句或先切片再判断。
int[] data = { 1, 9, 9, 5 };
// 正确:头为1,尾为5,中间任意
if (data is [1, .., 5])
{
Console.WriteLine("首尾符合");
}
// 错误示例(编译不过):试图跳过中间直接指定末尾前一位
// if (data is [1, .., 9, 5]) { }
性能方面,列表模式的匹配在编译期会展开为对Length和索引器的顺序访问,时间复杂度是O(n)且不会创建多余的枚举器。相比先用ToList再LINQ链式处理,它在分配上更友好。但在极热路径中,若集合很大且只关心头部几个元素,显式写索引判断可能比完整切片捕获更省内存,因为切片捕获会生成新数组。
五、小结与实践建议
列表模式让C#在集合处理上补齐了“结构性匹配”的短板。日常开发中,凡是出现“判断数组前几个元素”“按集合形状分支”“拆解命令参数”的逻辑,都可以优先用is [ ... ]或switch中的列表模式改写。它带来的不只是代码变短,更是意图表达的清晰化。
建议在团队代码规范中,将传统的arr.Length == x && arr[0] == y类判断逐步替换为列表模式,并对新人说明切片模式只能出现一次、被匹配对象需支持索引器等约束,避免误用导致编译错误。熟练掌握后,你会发现很多原本啰嗦的集合校验都能收敛成几行声明式代码。