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‘ with a collection expression because the type is not c
onstructible
error CS9174: Cannot initialize type ‘IReadOnlySet‘ with a collection expression because the type
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 类型的参数,按值传递,并且存在从 E 到集合类型的迭代类型的标识转换。
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, 也可以直接在类型中定义一个 IEnumerator GetEnumerator() 实例方法,也可以是一个扩展方法,更倾向于定义一个扩展方法或者实例方法,因为有些序列化的程序会把实现了 IEnumerable 的类型直接序列化成一个数组,如果有其他的成员信息,实现 IEnumable 的时候要小心一些

目前一些接口的实现类型还在优化中,.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

文档更新时间: 2023-09-23 06:15   作者:admin