很多语言,包括但不限于Python、Swift,都对内置的集合类型提供了各种方便的字面量写法:
l = [1,2,3]
传统C#集合初始化语法
要初始化一个集合,在C#中可以使用类似于C++初始化列表的语法。但是数组有一定的特殊性,最简单的初始化语法实际上只能数组用:
int[] a = {1, 2, 3};
int[] ap = new int[]{1, 2, 3};
int[] app = new int[3]{1, 2, 3};
对于List<T>这种类型,实际上只能用new List<T>{}来初始化。好在C# 9.0之后,如果类型可以推断出来,new后面的类型就可以省略了:
List<int> l = new List<int>(){1, 2, 3};
List<int> lp = new (){1, 2, 3};
对于非数组来说,这种写法:
List<int> l = new (){1,2,3};
等价于:
List<int> list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);
这一事实暗含了要使用{}初始化集合(实现了IEnumerable<T>的类型),就必须提供一个Add方法。
C#中的集合表达式
在C#12中,C#增加了集合表达式语法。最直接的好处就是,大部分标准库中的集合类型都支持大家在很多语言中喜闻乐见的[1, 2, 3]写法了。
int[] a = [1, 2, 3];
List<int> l = [1, 2, 3];
Stack<int> = [1, 2, 3];
展开集合
有时候可能会需要用一个现有的集合来初始化另外一个集合。集合表达式也有一个配套的语法:
int[] a = [1, 2, 3];
int[] b = [7, 8, 9];
int[] c = [..a, 4, 5, 6, ..b];
为自己的类型实现对集合表达式的支持
C#在这里还有一点好就是可以自行为自己的类型实现对集合表达式的支持。当然,微软的文档里表示一个“表现良好”(well-behaved)的集合表达式实现还是有一些要求,但是很多点不是强制的。
下面举个简单的例子。假设你设计了一个二维向量,然后希望让它可以支持Vector2D v = [1,2]这种写法。先简单定义一下这个struct:
record struct Vector2D(double X, double Y) : IEnumerable<double>
{
public IEnumerator<double> GetEnumerator()
{
return (IEnumerator<double>)new double[]{X, Y}.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
“集合表达式”毕竟是为集合考虑的,所以至少要让你的类型实现IEnumerable<T>。这里只是演示一下,具体的实现就随便一点了。
实现起来实际上很简单。首先你需要写一个参数为ReadOnlySpan<T>、返回你的类型的方法。这个参数就相当于是集合表达式传给你的各个元素,然后你需要用它们来构造你的类型的实例。在这个例子中我们简单地取表达式中的前两个元素作为向量的两个分量:
public static Vector Create(ReadOnlySpan<double> components)
{
return new Vector(components[0], components[1]);
}
第二步则是给包含此方法的类型加上一个[CollectionBuilder]特性。此特性主要有两个参数。第一个参数指的是此类型会作为哪个类型的builder。当然,官方文档中的例子是用了一个单独的类来放这个方法,但是实际上在同一个类中也行。第二个参数指的是哪个方法用来构造,用的是方法名来指定的:
[CollectionBuilder(typeof(Vector2D), "Create")]
record struct Vector2D(double X, double Y) : IEnumerable<double>{}
至此,工作就完成了。我们可以用集合表达式来初始化它了:
Vector2D v = [1, 2];
DLC:用集合表达式初始化接口
喜欢写C#的朋友都知道,我们可以把某个实现了某接口的对象放到目标类型为接口类型的地方。Sytem.Collection.Genrics中有很多表示集合行为的接口,实际上这些接口也能用这些集合表达式来初始化。
当然你一定也知道,用来初始化一个类型为接口类型的变量时,你的右值至少在某个地方得是一个具体的类型。然而集合表达式本身是不带类型信息的,毕竟它可以应用到不同类型的初始化上。
IEnumerable<int> e = [];
IEnumerable<int> ep = [1];
ICollection<int> c = [];
上面的代码在支持C#12的配置中都是合法的。但是编译器为这三行代码生成的代码是完全不同的。
IEnumerable<T>基本上算是最抽象的迭代器接口,只要某个对象能迭代就行。它不假设实现者有修改集合的能力。第一行实际上就是用空数组来初始化的。而第二行说到底也是用数组,但是如果你感兴趣可以去看看生成的代码实际上多了很多东西。至于ICollection<T>,它需要实现者提供增加和删除元素的能力,因此自然就会用此类需求最通用的List<T>来初始化了。