- 通过支持集合初始化器添加集合表达式支持
- 使用 CollectionBuilder 创建集合
- 使用 CollectionBuilder 处理泛型集合
- 将 CollectionBuilder 添加到接口
- 在早期框架版本中使用 CollectionBuilder
- 当事情并不完全如预期那样工作...
- 不支持集合表达式的内置类型
通过支持集合初始化器添加集合表达式支持
集合表达式会自动与任何支持集合初始化器的具体类型兼容。例如,Roslyn 中没有针对 HashSet
的特殊代码,但您可以使用它与集合表达式一起:
HashSet<int> values = [1, 2, 3, 4];
实际上,集合表达式生成的代码看起来有点像这样:
HashSet<int> values = new HashSet<int>();
values.Add(1);
values.Add(2);
values.Add(3);
values.Add(4);
values.Add(5);
这与使用集合初始化器版本生成的代码相同:
HashSet<int> values = new () { 1, 2, 3, 4 };
这适用于任何支持集合初始化器且具有公共无参数构造函数的类型——该类型也自动支持集合表达式。
注意,这里我只讨论了 “列表风格” 的集合初始化器;集合表达式还不支持 “字典风格” 的集合。
要支持集合初始化器所需的最低要求是实现 IEnumerable
,具有公共无参数构造函数,并且有一个公共 Add(T value)
实例方法,其中 T
是正确的类型(或者可以被通用地类型化)。
例如,下面展示了可以与集合初始化器一起使用的自定义集合的最小实现:
public class MyCollection : IEnumerable // 实现非泛型 IEnumerable
{
// 存储数据的备份集合
private readonly List<int> _list = new ();
// 实现 IEnumerable 所需成员
public IEnumerator<int> GetEnumerator() => _list.GetEnumerator();
// 集合初始化器所需的 Add() 方法
public void Add(int val)
{
_list.Add(val);
}
}
注意,我实现了非泛型的 IEnumerable
(而不是 IEnumerable<T>
或 IEnumerable<int>
之类的泛型版本),但这仅仅是因为它是最低要求;你当然可以(并且可以说应该)实现泛型版本。
有了上面的代码,现在可以使用集合初始化器语法创建集合:
MyCollection<int> myCollection = new () { 1, 2, 3, 4 };
这意味着现在也可以使用集合表达式:
MyCollection<int> myCollection = [1, 2, 3, 4];
这非常酷,但也有一些权衡。例如,即使使用集合表达式我们事先就知道元素的数量,但编译器(或 MyCollection
类型)无法使用这些信息来优化代码。相反,你必须重复调用 Add()
。
此外,使用集合初始化器意味着你被迫使用可变类型,因为类型是使用 new()
创建的,然后通过重复调用 Add
进行变异。如果你有选择不这样做就好了。
不幸的是,对于集合初始化器来说,你别无选择,没有满足这些要求所需的 API 的方法。但对于集合表达式,我们有机会!
使用 [CollectionBuilder]
创建集合
[CollectionBuilder]
属性是作为 C#12 集合表达式功能的一部分引入的,为您的类型支持集合表达式提供了一种高效机制。最容易的做法是展示一个集合构建器的最小实现,然后讨论其限制、要求和我们可以扩展的方式。
支持 [CollectionBuilder]
的 MyCollection
的最简单实现可能如下所示:
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
// 使用[CollectionBuilder]装饰类型,指向
// 集合表达式应该调用以创建类型的方法
[CollectionBuilder(typeof(MyCollection), nameof(Create))]
public class MyCollection
{
// 👇 这是集合表达式调用的方法
// 它必须接受一个只读跨度值并返回一个集合实例
public static MyCollection Create(ReadOnlySpan<int> values) => new (values);
private readonly int[] _values;
public MyCollection(ReadOnlySpan<int> values)
{
// 因为所有值都在构造函数中提供,我们可以
// 使用数组备份类型而不是列表,这样更高效
// 创建,并且不需要暴露突变的Add()方法
_values = values.ToArray();
}
// 必须有一个返回IEnumerator实现的GetEnumerator()方法
public IEnumerator<int> GetEnumerator() => _values.AsEnumerable().GetEnumerator();
}
这里有几个重要的点:
[CollectionBuilder]
属性指向一个Type
和一个用于创建装饰集合的命名方法。- 集合必须是一个 “迭代类型”,即它应该有一个
GetEnumerator()
方法,返回一个IEnumerator
(或IEnumerator<T>
)。- 通常你会通过实现
IEnumerable
来满足这一点,但我选择了上面的最小示例! - 由
GetEnumerator()
返回的类型T
定义了集合类型的 “元素类型” 。 - 如果你返回一个非泛型
IEnumerator
,则 “元素类型” 为object
。
- 通常你会通过实现
- 由
[CollectionBuilder]
属性指向的方法必须是static
,可访问(例如public
或internal
),并且接受一个类型为ReadOnlySpan<T>
的单个参数,其中T
是集合的 “元素类型” 。
遵循这些规则并添加 [CollectionBuilder]
后,集合很可能会创建得更加高效。当创建 ReadOnlySpan
时,编译器能够显著优化集合表达式,而这正是这里发生的情况。
为了演示它的实际效果,如果我们创建新实现的集合的新实例:
MyCollection myCollection = [1, 2, 3, 4];
那么 生成的代码 比使用集合初始化器版本时要高效得多。
MyCollection myCollection = MyCollection.Create(RuntimeHelpers.CreateSpan<int>((RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/));
internal sealed class <PrivateImplementationDetails>
{
[StructLayout(LayoutKind.Explicit, Pack = 4, Size = 16)]
internal struct __StaticArrayInitTypeSize=16_Align=4
{
}
internal static readonly __StaticArrayInitTypeSize=16_Align=4 CF97ADEEDB59E05BFD73A2B4C2A8885708C4F4F70C84C64B27120E72AB733B724/* Not supported: data(01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00) */;
}
除此之外,我们不需要暴露 Add()
突变方法(如果我们不想的话),而是在类型构造时直接给出所有元素。
使用 [CollectionBuilder]
处理泛型集合
上面展示的 MyCollection
示例有些牵强,因为它仅支持创建 int
类型的集合。我们现在使用的大多数集合都是泛型的,带有一个类型参数 T
。如果你想要装饰的集合是泛型的,需要做一些更改。
- 将
Create()
方法移到单独的非泛型类型中。[CollectionBuilder]
引用的类型不能是泛型的。 - 更新
Create()
方法使其成为泛型方法,接受ReadOnlySpan<T>
。 - 更新
GetEnumerator()
调用以返回IEnumerator<T>
(或者更好的做法是实现IEnumerable<T>
)
更新后的最小代码可能如下所示:
public static class MyCollectionBuilder
{
// 构建方法必须是泛型的,
// 但不能位于泛型类型中
public static MyCollection<T> Create<T>(ReadOnlySpan<T> values) => new (values);
}
// 更新[CollectionBuilder]以指向其他类型
[CollectionBuilder(typeof(MyCollectionBuilder), nameof(MyCollectionBuilder.Create))]
public class MyCollection<T>(ReadOnlySpan<T> values) // 现在它是一个泛型类型
{
// 使用主构造函数简化初始化泛型T[]
private readonly T[] _values = values.ToArray();
// 返回IEnumerator
public IEnumerator<T> GetEnumerator() => _values.AsEnumerable().GetEnumerator();
}
从使用角度来看,代码看起来大致相同,我们只是改变了类型使之成为泛型的,因此我们从 MyCollection
移动到了 MyCollection<T>
:
MyCollection<int> myCollection = [1, 2, 3, 4];
生成的代码 基本上与以前的非泛型实现相同,但现在该类型可以与任何元素类型一起工作!
将 [CollectionBuilder]
添加到接口
到目前为止,我们一直在具体类型上添加 [CollectionBuilder]
,但集合表达式的一个很好的特性是你也可以用它们与接口一起使用,例如:
IList<int> = [1, 2, 3, 4];
ICollection<int> = [1, 2, 3, 4];
如果你在自己的接口上应用 [CollectionBuilder]
属性,你也可以做同样的事情!让我们扩展前面的例子,同时使用一个接口。首先,我们将让我们的 MyCollection
类型实现接口 IMyCollection
:
[CollectionBuilder(typeof(MyCollection), nameof(Create))]
public class MyCollection<T> : IMyCollection<T> // 实现接口
{
// ... 如前
}
在这里使接口成为泛型的,但这不是必需的。接口的定义如下所示。它目前只是一个标记接口,唯一的要求是:
- 它装饰有
[CollectionBuilder]
属性,该属性指向有效构造方法(其中方法的返回类型为IMyCollection<T>
)。 - 它实现了
IEnumerable<T>
(给出元素类型为T
)或IEnumerable
(给出元素类型为object
)。
因此,向 IMyCollection
类型添加支持就像这样简单:
[CollectionBuilder(typeof(MyCollectionBuilder), nameof(MyCollectionBuilder.Create))]
public interface IMyCollection<T> : IEnumerable<T>
{
}
现在你可以像使用具体类型一样使用集合表达式与接口类型!
IMyCollection<int> myCollection = [1, 2, 3, 4];
在早期框架版本中使用 [CollectionBuilder]
CollectionBuilderAttribute
类型是随着 C#12
和 .NET 8
一起添加的,但如果您的目标是早期版本的 .NET
,或者您正在为多个运行时进行多目标开发,该怎么办?好消息是,您只需在项目中创建自己的 CollectionBuilderAttribute
版本即可满足编译器的要求!
只需将以下文件添加到您的项目中,突然之间您的集合表达式就可以在早期版本的 .NET
中工作,甚至可以在 .NET Framework
中工作!
#if !NET8_OR_GREATER
namespace System.Runtime.CompilerServices;
sealed class CollectionBuilderAttribute : Attribute
{
public CollectionBuilderAttribute(Type builderType, string methodName)
{
BuilderType = builderType;
MethodName = methodName;
}
public Type BuilderType { get; }
public string MethodName { get; }
}
#endif
注意,如果你的目标是旧版本的 .NET Framework
,你还需添加对 System.Memory 的引用,以便能够使用 ReadOnlySpan<T>
。
这几乎涵盖了关于创建自己的集合表达式的所有内容,但在结束之前,我们将简要回顾一下在尝试实现集合构建器时可能遇到的错误。
当事情并不完全如预期那样工作...
当我最初探索集合表达式的最小要求时,我遇到了一些麻烦。我正在探索最小(非泛型)版本,但我无法让我的代码编译。
这是我开始时的代码:
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
// 创建集合
MyCollection myCollection = [1, 2, 3, 4];
// 定义
[CollectionBuilder(typeof(MyCollection), nameof(Create))]
public class MyCollection(ReadOnlySpan<int> values)
{
// 构建方法
public static MyCollection Create(ReadOnlySpan<int> values) => new (values);
private readonly int[] _values = values.ToArray();
}
但我收到了错误
error CS9188: 'MyCollection' has a CollectionBuilderAttribute but no element type.
我开始围绕 CollectionBuilderAttribute
查找,试图找到一个我可以传递的 element
参数或类似的东西,但并没有这样的东西 🤷♂️ 希望阅读了前面的部分后,答案就会显而易见。我在搜索错误时找到了一条提示:
集合类型必须有一种迭代类型。换句话说,你可以将该类型作为集合来进行 foreach 遍历。
我知道 foreach
使用鸭子类型来寻找 GetEnumerator()
方法(而不是要求实现特定的方法),所以我尝试向类型添加GetEnumerator()
(我现在将省略 using
和调用以避免冗余):
[CollectionBuilder(typeof(MyCollection), nameof(Create))]
public class MyCollection(ReadOnlySpan<int> values)
{
public static MyCollection Create(ReadOnlySpan<int> values) => new (values);
private readonly int[] _values = values.ToArray();
// 使类型成为“迭代类型”,以便可以在foreach中使用
public IEnumerator GetEnumerator() => _values.GetEnumerator();
}
这解决了 CS9188
类型的问题,但现在我收到了新的错误:
error CS9187: Could not find an accessible 'Create' method with the expected signature: a static method with a single parameter of type 'ReadOnlySpan<int>' and return type 'MyCollection'.
这让我一开始感到困惑,为什么它坚持 Create()
接受 ReadOnlySpan<int>
而不是 ReadOnlySpan<object>
?
我最终明白了——这是因为我在返回非泛型 IEnumerator
而不是 IEnumerator<int>
,所以编译器将 “元素类型” 视为 object
而不是 int
。
为了解决这个问题,我决定实现 IEnumerable<int>
,以免出错:
[CollectionBuilder(typeof(MyCollection), nameof(Create))]
public class MyCollection(ReadOnlySpan<int> values) : IEnumerable<int> // 实现IEnumerable<T>
{
public static MyCollection Create(ReadOnlySpan<int> values) => new (values);
private readonly int[] _values = values.ToArray();
// 实现IEnumerable
IEnumerator<int> IEnumerable<int>.GetEnumerator() => ((IEnumerable<int>)_values).GetEnumerator();
public IEnumerator GetEnumerator() => _values.GetEnumerator();
}
但这仍然不起作用 🤦♂️ 我仍然收到要求 ReadOnlySpan<int>
的相同 CS9187
错误 😕
经过长时间的盯着看,我终于意识到我做了什么。IEnumerable
也实现了 IEnumerable
,所以你需要实现两个 GetEnumerator()
方法:
IEnumerator GetEnumerator()
是IEnumerable
接口所必需的IEnumerator<T> GetEnumerator()
是IEnumerable<T>
接口所必需的
由于这两个方法具有相同的签名,因此您需要在类型中 明确地 实现其中至少一个。在我的代码中,我已经明确实现了 IEnumerable<int>
接口,这与通常的做法 相反。通过这样做,集合表达式只发现了 IEnumerable
版本,并将元素类型设置为 object
。为了修复它,我只需要翻转哪一个被明确实现:
[CollectionBuilder(typeof(MyCollection), nameof(Create))]
public class MyCollection(ReadOnlySpan<int> values): IEnumerable<int> // 实现 IEnumerable<T>
{
public static MyCollection Create(ReadOnlySpan<int> values) => new(values);
private readonly int[] _values = values.ToArray();
// 👇 需要隐式实现 IEnumerable 实现...
public IEnumerator<int> GetEnumerator() => ((IEnumerable<int>)_values).GetEnumerator();
// 👇 ... 并且明确实现 IEnumerable 实现
IEnumerator IEnumerable.GetEnumerator() => _values.GetEnumerator();
}
在做出更改后,我的代码最终编译成功了!🎉 还值得一提的是,以下两种选项 同样 有效:
- 明确实现
IEnumerable
和IEnumerable<T>
的 两个 方法。在这种情况下,集合表达式会自动选择更具体的IEnumerable<T>
版本来确定元素类型。 - 实现
IEnumerator<int> GetEnumerator()
方法 不 实现IEnumerable<T>
,如我在上一节的最小示例中所示。
您可能感兴趣的是,[CollectionBuilder]
属性也是 运行时 为某些 Immutable*
类型(例如 ImmutableArray
和 ImmutableList
)添加集合表达式支持的方式。
但并非 所有 集合都得到了这种支持。
不支持集合表达式的内置类型
最明显的不支持集合表达式的集合是字典,尽管它们支持集合初始化器。好消息是这些将在未来的 C# 版本中出现,主要问题是如何决定语法。
因此,目前所有的字典类型 Dictionary<,>
、ImmutableDictionary<,>
、SortedDictionary<,>
等都不支持集合表达式。但是,还有一些其他类型你 可能会 认为应该支持集合表达式,但无法编译。
ISet<T>
— 许多接口类型如IList<T>
和IReadOnlyCollection<T>
可以直接使用集合表达式,但你不能使用ISet<T>
。我没有深入探究是否有特定原因!FrozenSet<T>
—ImmutableSet<T>
支持集合表达式,那么为什么FrozenSet<T>
不支持?嗯,在 .NET 9 中,它是支持的!SortedList<TKey, TValue>
— 这个有点奇怪,因为它 是 一个列表,但它 实际上 像字典一样初始化,有两个泛型参数,并且有一个Add(key, value)
而不是value
。所以当我们在 .NET 9 中获得字典表达式时,我们可能会得到这个。PriorityQueue<TElement, TPriority>
— 我在最近的一个系列中讨论了优先队列,并展示了它们与 “标准” 列表的行为相当不同。它们也不支持集合初始化器,所以我对它们不支持集合表达式并不感到惊讶。ConcurrentQueue<T>
— 我不得不说,我 期望 它可以使用集合表达式,特别是因为ConcurrentBag<T>
支持它们。但是ConcurrentQueue<T>
也 不支持 集合初始化器语法,因为它没有Add()
方法(只有Enqueue()
方法)。因此,“默认” 的集合表达式模式是回退到集合初始化器,这意味着代码无法编译。ConcurrentStack<T>
— 就像ConcurrentQueue<T>
一样,ConcurrentStack<T>
没有Add()
方法(它有Push()
),所以它不支持集合初始化器,因此也不支持集合表达式。
最后三种类型 PriorityQueue<>
、ConcurrentQueue<>
和 ConcurrentStack<>
不支持集合初始化器,但如果它们使用了 [CollectionBuilder]
,它们肯定 可以 支持。所以谁知道呢,如果需求足够高,也许将来它们会得到支持!
Comments