集合表达式 - C# 语言参考

可以使用集合表达式来创建常见的集合值。 集合表达式是一种简洁的语法,在计算时,可以分配给许多不同的集合类型。 集合表达式在 [] 括号之间包含元素的序列。 以下示例声明 string 元素的 System.Span<T>,并将其初始化为星期几:

Span<string> weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
foreach (var day in weekDays)
{
    Console.WriteLine(day);
}

集合表达式可以转换为许多不同的集合类型。 第一个示例演示了如何使用集合表达式初始化变量。 以下代码显示了可以使用集合表达式的其他许多位置:

// Initialize private field:
private static readonly ImmutableArray<string> _months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

// property with expression body:
public IEnumerable<int> MaxDays =>
    [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

public int Sum(IEnumerable<int> values) =>
    values.Sum();

public void Example()
{
    // As a parameter:
    int sum = Sum([1, 2, 3, 4, 5]);
}

不能使用需要编译时常量(例如初始化常量)或方法参数的默认值的集合表达式。

上述两个示例都使用常量作为集合表达式的元素。 还可以对元素使用变量,如以下示例所示:

string hydrogen = "H";
string helium = "He";
string lithium = "Li";
string beryllium = "Be";
string boron = "B";
string carbon = "C";
string nitrogen = "N";
string oxygen = "O";
string fluorine = "F";
string neon = "Ne";
string[] elements = [hydrogen, helium, lithium, beryllium, boron, carbon, nitrogen, oxygen, fluorine, neon];
foreach (var element in elements)
{
    Console.WriteLine(element);
}

分布元素

使用分布元素..在集合表达式中使用内联集合值。 以下示例通过将元音集合、同音集合和字母“y”组合在一起,为完整字母创建一个集合,该集合可以是以下任一类型:

string[] vowels = ["a", "e", "i", "o", "u"];
string[] consonants = ["b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
                       "n", "p", "q", "r", "s", "t", "v", "w", "x", "z"];
string[] alphabet = [.. vowels, .. consonants, "y"];

求值时,分布元素 ..vowels 产生五个元素:"a""e""i""o""u"。 分布元素 ..consonants 产生 20 个元素,即 consonants 数组中的数字。 分布元素中的变量必须使用 foreach 语句进行枚举。 如前面的示例所示,可以将分布元素与集合表达式中的单个元素组合在一起。

转换

集合表达式可以转换为不同的集合类型,包括:

重要

无论转换的目标类型如何,集合表达式总是会创建一个包含集合表达式中的所有元素的集合。 例如,当转换的目标为 System.Collections.Generic.IEnumerable<T> 时,生成的代码将计算集合表达式并将结果存储在内存中集合中。

这种行为不同于 LINQ;在 LINQ 中,在枚举序列之前可能不会将此序列实例化。 不能使用集合表达式生成不会枚举的无限序列。

编译器使用静态分析来确定使用集合表达式声明的集合的最高性能方法。 例如,如果初始化后不会修改目标,则可以将空集合表达式 [] 实现为 Array.Empty<T>()。 当目标为 System.Span<T>System.ReadOnlySpan<T> 时,存储可能是堆栈分配的。 集合表达式功能规范指定编译器必须遵循的规则。

许多 API 使用多个集合类型作为参数进行重载。 由于集合表达式可以转换为许多不同的表达式类型,因此这些 API 可能需要对集合表达式进行强制转换以指定正确的转换。 以下转换规则解决了一些歧义:

  • 转换为 Span<T>ReadOnlySpan<T> 或其他 ref struct 类型比转换为非 ref 结构类型更好。
  • 转换为非接口类型比转换为接口类型更好。

当集合表达式转换为 SpanReadOnlySpan 时,范围对象的安全上下文 取自范围中包含的所有元素安全上下文。 有关详细规则,请参阅集合表达式规范

集合生成器

集合表达式适用于行为良好的任何集合类型。 行为良好的集合具有以下特征:

  • 可计数集合上的 CountLength 值生成与枚举时元素数相同的值。
  • System.Collections.Generic 命名空间中的类型假定为无副作用。 因此,编译器可以优化这类类型可能用作中间值,但不会被公开的场景。
  • 如果调用集合中某些适用的 .AddRange(x) 成员,那么得到的最终值与循环访问 x 并使用 .Add 将其所有枚举值分别添加到该集合所得到的值相同。

.NET 运行时中的所有集合类型都行为良好。

警告

如果自定义集合类型行为不佳,则未定义将该集合类型与集合表达式一起使用时的行为。

类型通过编写 Create() 方法并对集合类型应用 System.Runtime.CompilerServices.CollectionBuilderAttribute 来指示生成器方法,选择使用集合表达式支持。 例如,请考虑使用固定长度缓冲区 80 个字符的应用程序。 该类可能类似于以下代码:

public class LineBuffer : IEnumerable<char>
{
    private readonly char[] _buffer = new char[80];

    public LineBuffer(ReadOnlySpan<char> buffer)
    {
        int number = (_buffer.Length < buffer.Length) ? _buffer.Length : buffer.Length;
        for (int i = 0; i < number; i++)
        {
            _buffer[i] = buffer[i];
        }
    }

    public IEnumerator<char> GetEnumerator() => _buffer.AsEnumerable<char>().GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => _buffer.GetEnumerator();

    // etc
}

你想要将其与集合表达式一起使用,如以下示例所示:

LineBuffer line = ['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!'];

LineBuffer 类型实现 IEnumerable<char>,因此编译器将其识别为 char 项的集合。 实现的 System.Collections.Generic.IEnumerable<T> 接口的类型参数指示元素类型。 需要向应用程序添加两个内容才能将集合表达式分配给 LineBuffer 对象。 首先,需要创建一个包含 Create 方法的类:

internal static class LineBufferBuilder
{
    internal static LineBuffer Create(ReadOnlySpan<char> values) => new LineBuffer(values);
}

Create 方法必须返回 LineBuffer 对象,并且必须采用 ReadOnlySpan<char> 类型的单个参数。 ReadOnlySpan 的类型参数必须与集合的元素类型匹配。 返回泛型集合的生成器方法将泛型 ReadOnlySpan<T> 作为其参数。 该方法必须可访问且为 static

最后,必须将 CollectionBuilderAttribute 添加到 LineBuffer 类声明:

[CollectionBuilder(typeof(LineBufferBuilder), "Create")]

第一个参数提供生成器类的名称。 第二个特性提供生成器方法的名称。