C# 12 新特性 collection expression
Intro
C# 12 中引入了一个 collection literal 或者叫 collection expression 的特性(起初叫 collection literal 后面改名叫 collection expression 了)
基本这一特性大部分的集合都可以使用 [1, 2, 3] 这样的语法来创建对象,对于想从一个集合类型改成另外一个类型就方便了很多,可以参考下面的示例
Get Started
简单示例如下:
int[] numArray = [1, 2, 3];
HashSet<int> numSet = [1, 2, 3];
List<int> numList = [1, 2, 3];
Span<char> charSpan = ['H', 'e', 'l', 'l', 'o'];
ReadOnlySpan<string> stringSpan = ["Hello", "World"];
ImmutableArray<string> immutableArray = ["Hello", "World"];
除了直接声明元素之外,也可以包含其他集合,使用 .. range 操作符来包含另外一个集合,示例如下:
int[] numArray = [1, 2, 3];
int[] nums = [1, 1, ..numArray, 2, 2];
Console.WriteLine(string.Join(",", nums));
输出结果如下:
1,1,1,2,3,2,2
集合个数不限于一个,也可以全是包含其他的集合,一个简单的示例如下:
int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[] single = [..row0, ..row1, ..row2];
foreach (var element in single)
{
Console.Write($"{element}, ");
}
输出结果如下:
1, 2, 3, 4, 5, 6, 7, 8, 9,
前面我们都是使用的具体的某一个类型,我们也可以使用一些接口,示例如下:
IEnumerable<string> enumerable = ["Hello", "dotnet"];
System.Console.WriteLine(enumerable.GetType());
IReadOnlyCollection<string> readOnlyCollection = ["Hello", "dotnet"];
System.Console.WriteLine(readOnlyCollection.GetType());
ICollection<string> collection = ["Hello", "dotnet"];
System.Console.WriteLine(collection.GetType());
// not supported for now
// ISet<string> set = ["Hello", "dotnet"];
// System.Console.WriteLine(set.GetType());
// IReadOnlySet<string> readonlySet = ["Hello", "dotnet"];
// System.Console.WriteLine(readonlySet.GetType());
IReadOnlyList<string> readOnlyList = ["Hello", "dotnet"];
System.Console.WriteLine(readOnlyList.GetType());
IList<string> list = ["Hello", "dotnet"];
System.Console.WriteLine(list.GetType());
IEnumerable<string> emptyEnumerable = [];
System.Console.WriteLine(emptyEnumerable.GetType());
IReadOnlyCollection<string> emptyReadonlyCollection = [];
System.Console.WriteLine(emptyReadonlyCollection.GetType());
ICollection<string> emptyCollection = [];
System.Console.WriteLine(emptyCollection.GetType());
IReadOnlyList<string> emptyReadOnlyList = [];
System.Console.WriteLine(emptyReadOnlyList.GetType());
IList<string> emptyList = [];
System.Console.WriteLine(emptyList.GetType());
那么使用接口时,实际类型会是什么呢,可以猜测一下,目前 ISet/IReadOnlySet 暂时不支持,会有一个类似下面这样的 error
error CS9174: Cannot initialize type ‘ISet
onstructible
error CS9174: Cannot initialize type ‘IReadOnlySet
is not constructible
当然这只是目前不可用,后面的版本也许会支持
上述代码输出结果如下:
图片
对比上面的代码可以看到,目前 emptyEnumerable/emptyReadonlyCollection/emptyReadOnlyList 目前是数组类型,其他的都是 List 类型,这是在 .NET 8 RC 1 版本上的结果,如果你是在 .NET 8 preview 7 你会发现结果会有所不同,会有更多的 list,在未来的版本的可能还会有变化,有些接口的实现类型可能会变成 ImmutableArray 的类型,感兴趣的朋友可以参考文末给出的一些链接了解一下
Custom collection Sample
除了框架自带的一些 collection,.NET 8 框架里提供一个 CollectionBuilderAttribute,使得我们也可以为自己的类型实现这一模式,
CollectionBuilderAttribute 定义如下:https://github.com/dotnet/runtime/blob/v8.0.0-rc.1.23419.4/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CollectionBuilderAttribute.cs
sealed class CollectionBuilderAttribute : Attribute
{
/// <summary>Initialize the attribute to refer to the <paramref name="methodName"/> method on the <paramref name="builderType"/> type.</summary>
/// <param name="builderType">The type of the builder to use to construct the collection.</param>
/// <param name="methodName">The name of the method on the builder to use to construct the collection.</param>
/// <remarks>
/// <paramref name="methodName"/> must refer to a static method that accepts a single parameter of
/// type <see cref="ReadOnlySpan{T}"/> and returns an instance of the collection being built containing
/// a copy of the data from that span. In future releases of .NET, additional patterns may be supported.
/// </remarks>
public CollectionBuilderAttribute(Type builderType, string methodName)
{
BuilderType = builderType;
MethodName = methodName;
}
/// <summary>Gets the type of the builder to use to construct the collection.</summary>
public Type BuilderType { get; }
/// <summary>Gets the name of the method on the builder to use to construct the collection.</summary>
/// <remarks>This should match the metadata name of the target method. For example, this might be ".ctor" if targeting the type's constructor.</remarks>
public string MethodName { get; }
}
第一个参数是 builder type,也就是定义创建方法的类型,第二个参数是 builder 的方法名称
自定义的话 collection type 必须要是可以迭代的,也就是可以 foreach 的
自定义需要满足下面几个条件,参考:https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md#create-methods
builder type 必须是一个非泛型的类型或者结构体
build method 必须是直接定义在 builder type 上的
build method 必须是一个静态方法
builder method 必须在使用 collection literal/expression 的地方是可以访问到的
builder method 方法的参数个数必须和类型个数一致
builder method 方法必须有一个 System.ReadOnlySpan
builder method 要可以有方法返回类型到集合类型的标识转换、隐式引用转换或装箱转换。
可以参考下面的这个例子
[CollectionBuilder(typeof(CustomCollectionBuilder), nameof(CustomCollectionBuilder.CreateNumber))]
file sealed class CustomNumberCollection : IEnumerable<int>
{
public required int[] Numbers { get; init; }
public IEnumerator<int> GetEnumerator()
{
return (IEnumerator<int>)Numbers.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return Numbers.GetEnumerator();
}
}
[CollectionBuilder(typeof(CustomCollectionBuilder), nameof(CustomCollectionBuilder.Create))]
file sealed class CustomCollection<T>
{
public required T[] Elements { get; init; }
// public IEnumerator<T> GetEnumerator()
// {
// return (IEnumerator<T>)Elements.GetEnumerator();
// }
}
file static class CustomCollectionBuilder
{
public static IEnumerator<T> GetEnumerator<T>(this CustomCollection<T> collection)
=> (IEnumerator<T>)collection.Elements.GetEnumerator();
public static CustomNumberCollection CreateNumber(ReadOnlySpan<int> elements)
{
return new CustomNumberCollection()
{
Numbers = elements.ToArray()
};
}
public static CustomCollection<T> Create<T>(ReadOnlySpan<T> elements)
{
return new CustomCollection<T>()
{
Elements = elements.ToArray()
};
}
}
使用示例和最初的例子类似:
CustomNumberCollection customNumberCollection = [1, 2, 3];
System.Console.WriteLine(string.Join(",", customNumberCollection.Numbers));
CustomCollection<string> customCollection = [ "Hello", "World" ];
System.Console.WriteLine(string.Join(",", customCollection.Elements));
输出结果如下:
1,2,3
Hello,World
More
仔细留意的话会发现示例里有多种实现迭代的方式,我们可以让自定义的 collection type 实现 IEnumerable
目前一些接口的实现类型还在优化中,.NET 8 正式版中可能会和现在的实现有所不同
References
https://github.com/dotnet/runtime/blob/51ccb5f6553308845aeb24672504b6f59c24b4c9/src/libraries/System.Linq.Expressions/src/System/Runtime/CompilerServices/ReadOnlyCollectionBuilder.cs#L15
https://github.com/dotnet/runtime/blob/51ccb5f6553308845aeb24672504b6f59c24b4c9/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CollectionBuilderAttribute.cs#L12
https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md
https://github.com/dotnet/csharplang/issues/5354
https://github.com/dotnet/roslyn/issues/68785
https://github.com/dotnet/roslyn/issues/69950
https://github.com/dotnet/runtime/blob/v8.0.0-rc.1.23419.4/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CollectionBuilderAttribute.cs
https://github.com/WeihanLi/SamplesInPractice/blob/master/CSharp12Sample/CollectionLiteralSample.cs