集合表达式

注意

本文是特性规范。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 ECMA 规范。

功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的 语言设计会议记录(LDM)中。

可以在有关规范的文章中了解更多有关将功能规范子块纳入 C# 语言标准的过程。

支持者问题:https://github.com/dotnet/csharplang/issues/8652

总结

集合表达式引入了一种新的简洁语法,即 [e1, e2, e3, etc],用于创建常见的集合值。 使用扩展元素 ..e 可以将其他集合内嵌到这些值中,如下所示:[e1, ..c2, e2, ..c2]

可以创建多个类似集合的类型,而无需使用外部 BCL 支持。 这些类型包括:

通过可在类型本身直接采用的新属性和 API 模式,可为上述未涵盖的类集合类型提供进一步支持。

动机

  • 类集合值广泛存在于编程、算法,尤其是 C#/.NET 生态系统中。 几乎所有程序都会利用这些值来存储数据,并从其他组件发送或接收数据。 目前,几乎所有 C# 程序都必须使用许多不同且不幸地冗长的方法来创建这类值的实例。 某些方法还存在性能缺陷。 以下是一些常见示例:

    • 数组,在 { ... } 值之前需要 new Type[]new[]
    • 范围,可能会使用 stackalloc 和其他繁琐的结构。
    • 集合初始值设定项,在其值之前需要使用 new List<T>这样的语法(可能会遗漏详细的 T),而且由于它们在不提供初始容量的情况下使用 N 次 .Add 调用,可能会导致内存的多次重新分配。
    • 不可变集合,需要使用 ImmutableArray.Create(...) 这样的语法来初始化值,并可能导致中间分配和数据复制。 效率更高的构造形式(如 ImmutableArray.CreateBuilder)不仅笨重,还会产生不可避免的垃圾。
  • 纵观周围的生态系统,我们还发现随处可见创建列表更方便、更易用的例子。 TypeScript、Dart、Swift、Elm、Python 等语言选择了一种简洁的语法形式,以此目的,广泛使用且效果显著。 初步调查显示,在这些生态系统中内置这些文字并没有引发实质性问题。

  • C# 还在 C# 11 中添加了 列表模式。 此模式使用简洁直观的语法,可以匹配和解构类似列表的值。 然而,与几乎所有其他模式构造不同,这种匹配/解构语法缺乏相应的构造语法。

  • 在构建每种集合类型时,要获得最佳性能可能很棘手。 简单的解决方案往往会浪费 CPU 和内存。 使用字面形式可以使编译器在实现优化时具有最大程度的灵活性,以产生至少与用户能提供的结果一样优良的结果,同时保持代码的简单性。 编译器往往能做得更好,而规范的目的就是在实施策略方面为实施留出很大的余地,从而确保这一点。

C# 需要包容性解决方案。 就客户已有的类似集合的类型和值而言,它应能满足客户的绝大多数需求。 它还应让语言感觉自然,并反映模式匹配中的工作。

这自然得出结论,语法应该类似于 [e1, e2, e3, e-etc][e1, ..c2, e2],因为它们对应于 [p1, p2, p3, p-etc][p1, ..p2, p3]的模式等效项。

详细设计

添加了以下语法规则:

primary_no_array_creation_expression
  ...
+ | collection_expression
  ;

+ collection_expression
  : '[' ']'
  | '[' collection_element ( ',' collection_element )* ']'
  ;

+ collection_element
  : expression_element
  | spread_element
  ;

+ expression_element
  : expression
  ;

+ spread_element
  : '..' expression
  ;

集合字面量由目标确定类型

规范说明

  • 为简洁起见,collection_expression 在下文中称为“字面量”。

  • expression_element 实例通常被称为 e1e_n 等。

  • spread_element 实例通常被称为 ..s1..s_n 等。

  • 范围类型表示 Span<T>ReadOnlySpan<T>

  • 文字通常显示为 [e1, ..s1, e2, ..s2, etc],以表达任意数量、任意顺序的元素。 需要注意的是,这个表格将用于展示所有的情况,例如:

    • 空字面量 []
    • 其中没有 expression_element 的字面量。
    • 其中没有 spread_element 的字面量。
    • 具有任何元素类型的任意排序的字面量。
  • ..s_n迭代变量的类型,如同 s_n 被用作 foreach_statement 中被迭代的表达式。

  • __name 开头的变量用于表示 name的计算结果,并将其存储在一个位置,以便只计算一次。 例如,__e1 是对 e1 的求值。

  • List<T>IEnumerable<T> 等指的是 System.Collections.Generic 命名空间中的相应类型。

  • 该规范定义了将字面量转换为现有 C# 结构的方法。 与查询表达式转换类似,只有在转换会产生合法代码的情况下,字面量本身才是合法的。 此规则的目的是避免重复其他隐含的语言规则(例如,关于表达式分配到存储位置时的可转换性)。

  • 不要求实现完全按照下面的规定来转换字面量。 如果生成的结果相同,且在产生结果的过程中没有可观察到的差异,则任何转换都是合法的。

    • 例如,实现可以将 [1, 2, 3] 等字面量直接转换为 new int[] { 1, 2, 3 } 表达式,该表达式本身会将原始数据嵌入到程序集中,而无需 __index 或一系列指令来分配每个值。 重要的是,这意味着如果转换的任何步骤在运行时可能导致异常,程序状态仍会保持转换所指示的状态。
  • 对“堆栈分配”的引用是指在堆栈而非堆上分配的任何策略。 重要的是,它并不意味着或要求策略必须通过实际的 stackalloc 机制。 例如,使用 内联数组 也是在可以进行的情况下实现栈分配的一种被允许且理想的方法。 请注意,在 C# 12 中,内联数组不能使用集合表达式来初始化。 这仍是一个未决提议。

  • 假定集合的行为良好。 例如:

    • 假定在枚举时,集合上的 Count 值将产生与元素个数相同的值。
    • 此规范中使用的在 System.Collections.Generic 命名空间中定义的类型被假定为无副作用。 因此,编译器可以优化这类类型可能用作中间值,但不会被公开的场景。
    • 我们假设,调用集合上某个适用的 .AddRange(x) 成员所产生的最终值,与遍历 x 并用 .Add 将其所有枚举值逐个添加到集合中的最终值相同。
    • 对于行为不佳的集合,集合字面量的行为未定义。

转换

集合表达式转换允许将集合表达式转换为类型。

从集合表达式到以下类型存在隐式集合表达式转换

  • 一维数组类型T[],在这种情况下,元素类型T
  • 范围类型
    • System.Span<T>
    • System.ReadOnlySpan<T>
      在这些情况下,元素类型T
  • 具有适当 创建方法类型,在这种情况下,元素类型 是由 实例方法或可枚举接口(而不是扩展方法)确定的 GetEnumerator
  • 实现System.Collections.IEnumerable 的结构或类类型,其中:
    • 类型有一个适用的构造函数,无需参数即可调用该构造函数,并且它还可在集合表达式的位置访问。

    • 如果集合表达式中有任何元素,类型就有一个实例或扩展方法 Add,其中:

      • 可以使用单个值参数调用该方法。
      • 如果方法是泛型的,则可以从集合和参数中推断出参数类型。
      • 该方法可在集合表达式的位置访问。

      在这种情况下,元素类型就是类型迭代类型

  • 接口类型:
    • System.Collections.Generic.IEnumerable<T>
    • System.Collections.Generic.IReadOnlyCollection<T>
    • System.Collections.Generic.IReadOnlyList<T>
    • System.Collections.Generic.ICollection<T>
    • System.Collections.Generic.IList<T>
      在这些情况下,元素类型T

如果类型具有元素类型U,那么对于集合表达式中的每个元素Eᵢ,都存在隐式转换:

  • 如果 Eᵢ表达式元素,则会从 Eᵢ 隐式转换为 U
  • 如果 Eᵢ 是一个分布元素..Sᵢ,那么 Sᵢ迭代类型将隐式转换为 U

没有从集合表达式到多维数组类型集合表达式转换

从集合表达式进行隐式集合表达式转换的类型是该集合表达式的有效目标类型

以下是集合表达式的其他隐式转换:

  • 可为 null 的值类型T?,其中有从集合表达式到值类型 T集合表达式转换。 转换过程是集合表达式转换T,然后是从 TT?隐式可为 Null 的转换

  • 对于引用类型 T,有一个与 关联的T,该方法返回一个类型 U和一个从 UT。 转换过程是集合表达式转换U,然后是从 UT

  • 对于接口类型 I,有一个与 关联的I,该方法返回一个类型 V和一个从 VI。 转换过程是集合表达式转换V,然后是从 VI

创建方法

创建方法通过[CollectionBuilder(...)]上的 属性表示。 该属性指定了一个方法的生成器类型方法名称,该方法将被调用来构造集合类型的实例。

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(
        AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface,
        Inherited = false,
        AllowMultiple = false)]
    public sealed class CollectionBuilderAttribute : System.Attribute
    {
        public CollectionBuilderAttribute(Type builderType, string methodName);
        public Type BuilderType { get; }
        public string MethodName { get; }
    }
}

属性可直接应用于 classstructref structinterface。 虽然该属性可应用于基础 classabstract class,但它不会被继承。

生成器类型必须是非泛型的 classstruct

首先,确定适用于创建方法集合。
它由符合以下要求的方法组成:

  • 该方法必须具有 [CollectionBuilder(...)] 属性中指定的名称。
  • 必须在生成器类型上直接定义该方法。
  • 该方法必须是 static
  • 使用集合表达式的地方必须能访问该方法。
  • 方法的 arity 必须与集合类型的 arity 匹配。
  • 该方法必须有一个 System.ReadOnlySpan<E> 类型的参数,该参数通过值传递。
  • 从方法返回类型到集合类型,存在标识转换隐式引用转换装箱转换

在基类型或接口上声明的方法将被忽略,它不属于 CM 集的一部分。

如果 CM 集为空,则 集合类型 没有 元素类型,也没有 create 方法。 以下步骤均不适用。

如果 CM 集中只有一个方法具有从 E集合类型元素类型标识转换,那么这个方法就是集合类型创建方法。 否则,集合类型就没有创建方法

如果 [CollectionBuilder] 属性未引用具有预期签名的可调用方法,则会报错。

对于具有目标类型 C<S0, S1, …>集合表达式,其中类型声明C<T0, T1, …>具有关联的生成器方法B.M<U0, U1, …>(),来自目标类型的泛型类型参数将按从最外层包含类型到最内层的顺序应用到生成器方法

创建方法的范围参数可以显式标记为 scoped[UnscopedRef]。 如果参数隐式或显式为scoped,则编译器可能会在堆栈而不是堆上为范围分配存储空间。

例如,ImmutableArray<T> 可能的创建方法

[CollectionBuilder(typeof(ImmutableArray), "Create")]
public struct ImmutableArray<T> { ... }

public static class ImmutableArray
{
    public static ImmutableArray<T> Create<T>(ReadOnlySpan<T> items) { ... }
}

通过上面的创建方法ImmutableArray<int> ia = [1, 2, 3]; 可以被发出为:

[InlineArray(3)] struct __InlineArray3<T> { private T _element0; }

Span<int> __tmp = new __InlineArray3<int>();
__tmp[0] = 1;
__tmp[1] = 2;
__tmp[2] = 3;
ImmutableArray<int> ia =
    ImmutableArray.Create((ReadOnlySpan<int>)__tmp);

建筑

集合表达式中的元素按从左到右的顺序进行计算。 每个元素只被评估一次,对元素的任何进一步引用都指的是这次初始评估的结果。

可以在集合表达式中的后续元素计算之前或之后迭代分布元素。

构建过程中使用的任何方法抛出的未处理异常都将不会被捕获,并将阻止构建过程中的后续步骤。

假设 LengthCountGetEnumerator 没有副作用。


如果目标类型是实现了 System.Collections.IEnumerable结构体类类型,并且目标类型没有创建方法,则集合实例的构造如下所示:

  • 各元素按顺序进行计算。 部分或全部元素可能会在以下步骤期间而不是之前进行计算。

  • 编译器 可能会通过在每个 分布元素表达式上调用 可计数的 属性(或来自已知接口或类型的等效属性)来确定集合表达式的已知长度

  • 适用的不带参数的构造函数被调用。

  • 每个元素的顺序为:

    • 如果元素是表达式元素,则以元素表达式作为参数调用适用的 Add 实例或扩展方法。 (与经典的集合初始值设定项行为不同,元素评估和 Add 调用并不一定交错进行。)
    • 如果元素是分布元素,则使用以下方法之一:
      • 分布元素表达式上调用适用的 GetEnumerator 实例或扩展方法,并在以项目为参数的集合实例上为枚举器中的每个项目调用适用的 Add 实例或扩展方法。 如果枚举器实现了 IDisposable,那么无论是否出现异常,枚举后都将调用 Dispose
      • AddRange 上调用适用的 实例或扩展方法,将展开元素 表达式 作为参数。
      • 在以集合实例和 int 索引为参数的分布元素表达式上调用适用的 CopyTo 实例或扩展方法。
  • 在上述构建步骤中,可能会在带有 int 容量参数的集合实例上调用一次或多次适用的 EnsureCapacity 实例或扩展方法。


如果目标类型是数组范围、具有创建方法接口的的类型,则集合实例的构造如下所示:

  • 各元素按顺序进行计算。 部分或全部元素可能会在以下步骤期间而不是之前进行计算。

  • 编译器 可能会通过在每个 分布元素表达式上调用 可计数的 属性(或来自已知接口或类型的等效属性)来确定集合表达式的已知长度

  • 创建初始化实例的过程如下:

    • 如果目标类型是数组,并且集合表达式具有已知长度,则会分配一个具有预期长度的数组。
    • 如果目标类型是范围或具有创建方法的类型,并且集合具有已知长度,则会参照连续存储创建一个具有预期长度的范围。
    • 否则将分配临时存储空间。
  • 每个元素的顺序为:

    • 如果元素是表达式元素,则会调用初始化实例索引器,以便在当前索引处添加已求值的表达式。
    • 如果元素是分布元素,则使用以下方法之一:
      • 调用一个众所周知的接口或类型的成员,把元素从 spread 元素表达式复制到初始化实例。
      • 分布元素表达式上调用适用的 GetEnumerator 实例或扩展方法,并为枚举器中的每个项目调用初始化实例索引器以添加当前索引中的项目。 如果枚举器实现了 IDisposable,那么无论是否出现异常,枚举后都将调用 Dispose
      • CopyTo 上调用适用的 实例或扩展方法,并以初始化实例和 int 索引作为参数。
  • 如果已为集合分配了中间存储空间,则会分配一个具有实际集合长度的集合实例,并将初始化实例中的值复制到集合实例中;如果需要范围,编译器可能会使用中间存储空间中实际集合长度的范围。 否则,初始化实例就是集合实例。

  • 如果目标类型具有创建方法,则会使用范围实例来调用创建方法。


注意: 编译器可能会延迟向集合中添加元素,或延迟迭代遍历分布元素,直到对后续元素完成求值。 (当后续的分布元素具有可计数属性,可以在分配集合之前计算集合的预期长度时)。相反,当延迟没有任何好处时,编译器可能会急于将元素添加到集合中,并急于遍历分布元素。

请考虑以下集合表达式:

int[] x = [a, ..b, ..c, d];

如果分布元素 bc可数的,则编译器可以延迟从 ab 中添加项,直到 c 被求值之后,以便按预期长度分配数组。 之后,编译器可以在评估 d 之前,即时地添加 c 中的项目。

var __tmp1 = a;
var __tmp2 = b;
var __tmp3 = c;
var __result = new int[2 + __tmp2.Length + __tmp3.Length];
int __index = 0;
__result[__index++] = __tmp1;
foreach (var __i in __tmp2) __result[__index++] = __i;
foreach (var __i in __tmp3) __result[__index++] = __i;
__result[__index++] = d;
x = __result;

空集合字面量

  • 空字面量 [] 没有类型。 但是,与 null 字面量类似,此字面量可以隐式转换为任何可构造集合类型。

    例如,由于不存在目标类型,也不涉及其他转换,因此下面的代码是不合法的:

    var v = []; // illegal
    
  • 允许省略展开空字面量。 例如:

    bool b = ...
    List<int> l = [x, y, .. b ? [1, 2, 3] : []];
    

    在此处,如果 b 为 false,则不需要为空集合表达式构造任何值,因为它会立即在最终字面中分散为零值。

  • 如果空集合表达式用于构建已知不可变的最终集合值,则允许其为单一实例。 例如:

    // Can be a singleton, like Array.Empty<int>()
    int[] x = []; 
    
    // Can be a singleton. Allowed to use Array.Empty<int>(), Enumerable.Empty<int>(),
    // or any other implementation that can not be mutated.
    IEnumerable<int> y = [];
    
    // Must not be a singleton.  Value must be allowed to mutate, and should not mutate
    // other references elsewhere.
    List<int> z = [];
    

Ref 安全性

有关安全上下文值:声明块, 函数成员以及调用方上下文的定义,请参阅安全上下文约束

集合表达式的 安全上下文 为:

  • 空集合表达式 [] 的安全上下文是调用方上下文

  • 如果目标类型是 跨度类型System.ReadOnlySpan<T>,且 T基元类型之一:boolsbytebyteshortushortcharintuintlongulongfloatdouble,并且集合表达式仅包含 常量值,那么集合表达式的安全上下文是 调用方上下文

  • 如果目标类型是范围类型System.Span<T>System.ReadOnlySpan<T>,则集合表达式的安全上下文是声明块

  • 如果目标类型是具有创建方法ref 结构类型,则集合表达式的安全上下文是调用创建方法的安全上下文,其中集合表达式是方法的范围参数。

  • 否则,集合表达式的安全上下文就是调用方上下文

安全上下文为声明块的集合表达式无法脱离封闭范围,编译器可能会将集合存储在堆栈而不是堆上。

要使 ref 结构类型的集合表达式摆脱声明块,可能需要将表达式强制转换为另一种类型。

static ReadOnlySpan<int> AsSpanConstants()
{
    return [1, 2, 3]; // ok: span refers to assembly data section
}

static ReadOnlySpan<T> AsSpan2<T>(T x, T y)
{
    return [x, y];    // error: span may refer to stack data
}

static ReadOnlySpan<T> AsSpan3<T>(T x, T y, T z)
{
    return (T[])[x, y, z]; // ok: span refers to T[] on heap
}

类型推理

var a = AsArray([1, 2, 3]);          // AsArray<int>(int[])
var b = AsListOfArray([[4, 5], []]); // AsListOfArray<int>(List<int[]>)

static T[] AsArray<T>(T[] arg) => arg;
static List<T[]> AsListOfArray<T>(List<T[]> arg) => arg;

类型推理规则更新如下。

第一阶段 的现有规则将被提取到一个新的 输入类型推理 节,并且在 输入类型推理输出类型推理 中为集合表达式添加了一项规则。

11.6.3.2 第一阶段

对于每个方法参数 Eᵢ

  • 输入类型推理Eᵢ相应的参数类型Tᵢ

表达式E类型 T按以下方式进行:

  • 如果 E 是具有元素 Eᵢ集合表达式,并且 T 是具有元素类型Tₑ的类型,或者 T可为 null 的类型T0?,并且 T0 具有元素类型Tₑ,那么对于每个 Eᵢ
    • 如果 Eᵢ表达式元素,则会进行EᵢTₑ
    • 如果 Eᵢ 是具有迭代类型Sᵢ分布元素,则会进行SᵢTₑ下限推理
  • [第一阶段中的现有规则]...

11.6.3.7 输出类型推理

表达式 E类型 T输出类型推论按以下方式进行:

  • 如果 E 是具有元素 Eᵢ集合表达式,并且 T 是具有元素类型Tₑ的类型,或者 T可为 null 的类型T0?,并且 T0 具有元素类型Tₑ,那么对于每个 Eᵢ
    • 如果 Eᵢ表达式元素,则会进行EᵢTₑ输出类型推断
    • 如果 Eᵢ 是一个分布元素,则不会从 Eᵢ 进行推理。
  • [输出类型推理中的现有规则] ...

扩展方法

不更改扩展方法调用规则。

12.8.10.3 扩展方法调用

如果出现以下情况,则扩展方法 Cᵢ.Mₑ 符合条件

  • ...
  • exprMₑ第一个参数的类型之间存在隐式标识、引用或装箱转换。

集合表达式没有自然类型,因此从 类型转换为 类型的现有转换不适用。 因此,集合表达式不能直接用作扩展方法调用的第一个参数。

static class Extensions
{
    public static ImmutableArray<T> AsImmutableArray<T>(this ImmutableArray<T> arg) => arg;
}

var x = [1].AsImmutableArray();           // error: collection expression has no target type
var y = [2].AsImmutableArray<int>();      // error: ...
var z = Extensions.AsImmutableArray([3]); // ok

重载决策

更好的从表达式转换已更新,以便在集合表达式转换中优先选择某些目标类型。

在更新的规则中:

  • span_type 是以下之一:
    • System.Span<T>
    • System.ReadOnlySpan<T>
  • array_or_array_interface 是以下之一:
    • 数组类型
    • 以下接口类型之一由数组类型实现:
      • System.Collections.Generic.IEnumerable<T>
      • System.Collections.Generic.IReadOnlyCollection<T>
      • System.Collections.Generic.IReadOnlyList<T>
      • System.Collections.Generic.ICollection<T>
      • System.Collections.Generic.IList<T>

给定从表达式 C₁ 转换为类型 E的隐式转换 T₁,以及从表达式 C₂ 转换为类型 E的隐式转换 T₂,当以下条件之一成立时,若 C₁ 是一种比 更好的 C₂ 转换:

  • E集合表达式,并且以下条件之一成立:
    • T₁System.ReadOnlySpan<E₁>T₂System.Span<E₂>,并且从 E₁E₂ 存在隐式转换
    • 是元素类型为数组或数组接口,且存在从 的隐式转换。
    • T₁ 不是 span_typeT₂ 不是 span_type,并且存在从 T₁T₂ 的隐式转换
  • E 不是 集合表达式,且以下任一条件成立:
    • ET₁ 完全匹配,ET₂ 不完全匹配
    • ET₁T₂都完全匹配或都不匹配,并且 T₁ 是一个比 T₂
  • E 是方法组,...

数组初始值设定项和集合表达式之间重载决策差异的示例:

static void Generic<T>(Span<T> value) { }
static void Generic<T>(T[] value) { }

static void SpanDerived(Span<string> value) { }
static void SpanDerived(object[] value) { }

static void ArrayDerived(Span<object> value) { }
static void ArrayDerived(string[] value) { }

// Array initializers
Generic(new[] { "" });      // string[]
SpanDerived(new[] { "" });  // ambiguous
ArrayDerived(new[] { "" }); // string[]

// Collection expressions
Generic([""]);              // Span<string>
SpanDerived([""]);          // Span<string>
ArrayDerived([""]);         // ambiguous

范围类型

范围类型 ReadOnlySpan<T>Span<T> 都是可构造集合类型。 支持它们遵循 params Span<T> 的设计。 具体而言,如果参数数组处于编译器设置的限制范围(如果存在)内,则构造任一此类范围将在 堆栈上创建一个数组 T[]。 否则,数组将在堆上分配。

如果编译器选择在堆栈上分配,则不需要在该特定点将字面量直接转换为 stackalloc。 例如,给定:

foreach (var x in y)
{
    Span<int> span = [a, b, c];
    // do things with span
}

只要 stackalloc 的含义保持不变,并且Span得到维护,编译器就可以使用 进行转换。 例如,它可以将上述内容转换为:

Span<int> __buffer = stackalloc int[3];
foreach (var x in y)
{
    __buffer[0] = a
    __buffer[1] = b
    __buffer[2] = c;
    Span<int> span = __buffer;
    // do things with span
}

在选择堆栈分配时,编译器还可以使用内联数组(如果可用)。 请注意,在 C# 12 中,内联数组不能使用集合表达式来初始化。 此功能是一项公开提议。

如果编译器决定在堆上分配,那么 Span<T> 的转换就很简单了:

T[] __array = [...]; // using existing rules
Span<T> __result = __array;

集合字面量转换

如果集合表达式中每个分布元素的编译时类型为可数,则集合表达式具有已知长度

接口转换

不可变接口翻译

如果目标类型(即 IEnumerable<T>IReadOnlyCollection<T>IReadOnlyList<T>)不包含可变成员,那么符合要求的实现就必须生成一个实现该接口的值。 如果合成了类型,则建议合成类型实现所有这些接口以及 ICollection<T>IList<T>,而不管针对的是哪个目标接口类型。 这确保了与现有库的最大兼容性,包括那些反省值所实现接口的库,以便进行性能优化。

此外,该值必须实现非泛型 ICollectionIList 接口。 这样,集合表达式就能在数据绑定等场景中支持动态自省。

符合要求的实施可以自由地:

  1. 使用实现所需接口的现有类型。
  2. 生成实现所需接口的类型。

在任何一种情况下,允许的类型可以实现比严格要求更多的接口集。

合成类型可以自由采用任何策略,以恰当地实现所需的接口。 例如,合成类型可以直接在自身内部内联元素,从而避免额外的内部集合分配。 合成类型也可以不使用任何存储空间,而是直接计算数值。 例如,为 index + 1 返回 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  1. 在查询 ICollection<T>.IsReadOnly(如已实现)和非泛型 IList.IsReadOnlyIList.IsFixedSize 时,该值必须返回 true。 这样,尽管实现了可变视图,但仍能确保使用者能正确判断集合是不可变的。
  2. 该值必须在任何调用突变方法(如 IList<T>.Add)时抛出。 这样可以确保安全,防止不可变集合发生意外变异。

可变接口转换

给定的目标类型包含可变成员,即 ICollection<T>IList<T>

  1. 该值必须是 List<T> 的实例。

已知长度转换

有了已知长度,就可以高效地构建结果,从而避免复制数据和在结果中出现不必要的空闲空间。

没有已知长度也并不会妨碍创建任何结果。 但是,这可能会产生额外的 CPU 和内存成本,因为要先生成数据,然后再移动到最终目标。

  • 对于已知长度字面量 [e1, ..s1, etc],转换首先从以下内容开始:

    int __len = count_of_expression_elements +
                __s1.Count;
                ...
                __s_n.Count;
    
  • 给定该字面量的目标类型 T

    • 如果 T 是某些 T1[],则字面量将转换为:

      T1[] __result = new T1[__len];
      int __index = 0;
      
      __result[__index++] = __e1;
      foreach (T1 __t in __s1)
          __result[__index++] = __t;
      
      // further assignments of the remaining elements
      

      允许实现利用其他方法来填充数组。 例如,利用 .CopyTo() 等高效的批量复制方法。

    • 如果 T 是某些 Span<T1>,则字面量转换与上述相同,只是 __result 初始化已转换为:

      Span<T1> __result = new T1[__len];
      
      // same assignments as the array translation
      

      如果要保持范围安全性,则转换可以使用 stackalloc T1[]内联数组,而不是 new T1[]

    • 如果 T 是某些 ReadOnlySpan<T1>,那么字面量转换与 Span<T1> 的情况相同,只是最终结果是Span<T1>隐式地转换ReadOnlySpan<T1>

      ReadOnlySpan<T1>,其中 T1 是某种基元类型,所有集合元素都是常量,它的数据不需要放在堆或栈上。 例如,实现可以将这个跨度直接构造为对程序数据段某部分的引用。

      上述形式(对于数组和范围)是集合表达式的基本表示形式,用于以下转换规则:

      • 如果 T 是某些具有相应创建方法B.M<U0, U1, …>()C<S0, S1, …>,则字面量将转换为:

        // Collection literal is passed as is as the single B.M<...>(...) argument
        C<S0, S1, …> __result = B.M<S0, S1, …>([...])
        

        由于创建方法必须具有某些实例化 ReadOnlySpan<T>的参数类型,因此将集合表达式传递给创建方法时,范围转换规则将适用。

      • 如果 T 支持集合初始值设定项,则:

        • 如果类型 T 包含一个带有单个参数 int capacity 的可访问构造函数,则字面量将转换为:

          T __result = new T(capacity: __len);
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          注意:参数名称必须是 capacity

          这种形式允许用文字来告知新建类型的元素数量,以便有效分配内部存储空间。 这可以避免在添加元素时不必要的重新分配。

        • 否则,字面量将被转换为:

          T __result = new T();
          
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          这允许创建目标类型,尽管没有优化容量以防止内部重新分配存储。

未知长度转换

  • T字面量的给定目标类型

    • 如果 T 支持集合初始值设定项,则字面量将转换为:

      T __result = new T();
      
      __result.Add(__e1);
      foreach (var __t in __s1)
          __result.Add(__t);
      
      // further additions of the remaining elements
      

      这样就可以传播任何可迭代类型,尽管需要尽可能少的优化。

    • 如果 T 是某些 T1[],那么该字面量与其语义相同:

      List<T1> __list = [...]; /* initialized using predefined rules */
      T1[] __result = __list.ToArray();
      

      但上述方法效率很低:它会首先创建中间列表,然后再从中创建最终数组的副本。 实现可以自由地进行优化,省略这部分内容,例如生成如下代码:

      T1[] __result = <private_details>.CreateArray<T1>(
          count_of_expression_elements);
      int __index = 0;
      
      <private_details>.Add(ref __result, __index++, __e1);
      foreach (var __t in __s1)
          <private_details>.Add(ref __result, __index++, __t);
      
      // further additions of the remaining elements
      
      <private_details>.Resize(ref __result, __index);
      

      这样可以尽量减少浪费和复制,也不会增大库集合可能产生的额外开销。

      传递给 CreateArray 的计数用于提供起始大小提示,以防止调整大小带来浪费。

    • 如果 T 是某种范围类型,那么实现方法可以遵循上述 T[] 策略,或语义相同但性能更好的任何其他策略。 例如,可以使用 CollectionsMarshal.AsSpan(__list) 直接获取范围值,而不是分配数组作为列表元素的副本。

不支持的方案

虽然集合字面量可用于许多方案,但有一些方案是它们无法替代的。 这些设置包括:

  • 多维数组(如 new int[5, 10] { ... })。 没有包含维度的设施,并且所有集合字面量都是线性结构或映射结构。
  • 向构造函数传递特殊值的集合。 没有方法访问正在使用的构造函数。
  • 嵌套集合初始值设定项,如 new Widget { Children = { w1, w2, w3 } }。 这种形式需要保留,因为它与 Children = [w1, w2, w3] 的语义截然不同。 前者会在 .Children 上重复调用 .Add,而后者会在 .Children 上分配一个新的集合。 我们可以考虑让后一种形式在 .Children 无法分配的情况下退回到添加到现有集合,但这似乎会造成极大的混乱。

语法歧义

  • 有两种“真正的”语法歧义,即使用 collection_literal_expression 的代码有多种合法的语法解释。

    • spread_elementrange_expression之间存在歧义。 从技术角度来说,可以有:

      Range[] ranges = [range1, ..e, range2];
      

      要解决此问题,我们可以:

      • 要求用户对 (..e) 加括号,如果需要范围,则需在 0..e 中包含起始索引。
      • 为分布选择不同的语法(如 ...)。 这将是切片模式缺乏一致性的遗憾。
  • 有两种情况并不存在真正的歧义,但语法会显著增大解析的复杂性。 虽然在工程时间充足的情况下问题不大,但用户在查看代码时,这仍会增加认知负担。

    • 语句或局部函数上的 collection_literal_expressionattributes 含混不清。 如下所示:

      [X(), Y, Z()]
      

      这可能是以下之一:

      // A list literal inside some expression statement
      [X(), Y, Z()].ForEach(() => ...);
      
      // The attributes for a statement or local function
      [X(), Y, Z()] void LocalFunc() { }
      

      如果没有复杂的预读,就不可能在不消耗整个字面量的情况下做出判断。

      解决这一问题的选项包括:

      • 允许这样做,进行分析工作以确定这是哪种情况。
      • 禁止这样做,并要求用户用括号将字面量括起来,如 ([X(), Y, Z()]).ForEach(...)
      • collection_literal_expression 中的 conditional_expressionnull_conditional_operations 之间有歧义。 如下所示:
      M(x ? [a, b, c]
      

      这可能是以下之一:

      // A ternary conditional picking between two collections
      M(x ? [a, b, c] : [d, e, f]);
      
      // A null conditional safely indexing into 'x':
      M(x ? [a, b, c]);
      

      如果没有复杂的预读,就不可能在不消耗整个字面量的情况下做出判断。

      注意:即使没有自然类型,这也是一个问题,因为目标类型在 conditional_expressions中生效。

      和其他方法一样,我们可以要求使用括号来消除歧义。 换言之,假定 null_conditional_operation 解释,除非是这样写:x ? ([1, 2, 3]) :。 然而,这似乎相当不幸。 这种代码编写起来似乎不算不合理,但可能会使人感到困惑。

缺点

  • 这就在我们已有的无数种方式之外,为集合表达式引入了另一种形式。 这会增加语言的复杂性。 尽管如此,这也使得统一一种 ring语法成为可能,这意味着现有的代码库可以简化,并在所有地方使用统一的外观。
  • 使用 [...] 代替 {...} 偏离了我们通常用于数组和集合初始化器的语法。 具体来说,它使用 [...] 而不是 {...}。但是,语言团队在设计列表模式时已经确定了这一点。 我们试图让 {...} 与列表模式兼容,但遇到了无法克服的问题。 正因为如此,我们改用了 [...],这种方法虽然对 C# 来说是新的,但在许多编程语言中都很自然,让我们可以从头开始,没有任何歧义。 使用 [...] 作为相应的字面形式与我们的最新决策互补,并为我们创造一个没有问题的干净工作环境。

这确实为语言引入了瑕疵。 例如,以下这两个例子都是合法的,并且(幸运的是)它们的含义完全相同:

int[] x = { 1, 2, 3 };
int[] x = [ 1, 2, 3 ];

但是,鉴于新的字面量语法所带来的广泛性和一致性,我们应考虑建议大家改用新的形式。 IDE 的建议和修复可能会在这方面有所帮助。

替代方案

  • 还考虑过哪些其他设计? 不这样做会有什么影响?

已解决的问题

  • 内联数组不可用,而迭代类型是基元类型时,编译器是否应该使用 stackalloc 进行堆栈分配?

    解决方法:无。 与内联数组相比,管理 stackalloc 缓冲区需要额外的工作,以确保在循环中使用集合表达式时不会重复分配缓冲区。 在旧平台上,编译器和生成代码的额外复杂性超过了堆栈分配的好处。

  • 与 Length/Count 属性求值相比,应按什么顺序来计算字面量元素? 我们是否应该先对所有元素求值,然后再对所有长度求值? 我们应该先计算一个元素,然后计算其长度,再计算下一个元素,依次进行吗?

    解决方法:我们首先评估所有元素,然后其他的一切都会随之进行。

  • 一个未知长度字面量能否创建一个需要已知长度的集合类型,例如数组、范围或 Construct(array/span) 集合? 要高效地做到这一点比较困难,但通过巧妙地使用公用数组和/或生成器还是有可能的。

    解决方法:是,我们允许从未知长度字面量创建一个固定长度集合。 编译器可以尽可能高效地实现这一点。

    下面的文本用于记录此话题的原始讨论。

    用户总是可以使用如下代码将未知长度字面量转换为已知长度字面量:

    ImmutableArray<int> x = [a, ..unknownLength.ToArray(), b];
    

    然而,由于需要强制分配临时存储空间,这种情况令人遗憾。 如果我们能控制其发出的方式,就有可能提高效率。

  • collection_expression 能否根据 IEnumerable<T> 或其他集合接口由目标确定类型?

    例如:

    void DoWork(IEnumerable<long> values) { ... }
    // Needs to produce `longs` not `ints` for this to work.
    DoWork([1, 2, 3]);
    

    解决方法:是,字面量可以根据 I<T> 实现的任何接口类型 List<T> 由目标确定类型。 例如,IEnumerable<long>。 这相当于将目标类型转换为 List<long>,然后将结果分配给指定的接口类型。 下面的文本用于记录此话题的原始讨论。

    当前的问题是确定要实际创建的基础类型。 一种方法是查看 params IEnumerable<T> 的提案。 在这里,我们将生成一个数组来传递数值,与 params T[] 的情况类似。

  • 编译器能否/是否应该为 Array.Empty<T>() 输出 []? 我们是否应该强制要求它这样做,以尽可能避免分配?

    是的。 在任何情况下,如果这样做是合法的,且最终结果是不可变的,编译器都应发出 Array.Empty<T>()。 例如,针对 T[]IEnumerable<T>IReadOnlyCollection<T>IReadOnlyList<T>。 当目标是可变的(ICollection<T>IList<T>)时,不应使用 Array.Empty<T>

  • 我们应该扩展集合初始化器来查找非常常见的 AddRange 方法吗? 基础构造类型可以使用它来更有效地执行扩展元素的添加。 我们可能还希望寻找类似 .CopyTo 的内容。 与直接在转换代码中进行枚举相比,这些方法最终可能会导致过多的分配/调度,因此可能会存在一些弊端。

    是的。 允许实现使用其他方法来初始化集合值,但前提是这些方法具有定义明确的语义,而且集合类型应“表现良好”。 不过,在实践中,进行实现时应当谨慎,因为一种途径(例如批量复制)的好处可能伴随着负面影响(例如,对结构集合进行装箱)。

    在没有任何不利因素的情况下,实现应充分利用这些因素。 例如,使用 .AddRange(ReadOnlySpan<T>) 方法。

未解决的问题

  • 迭代类型“模棱两可”(根据某种定义)时,我们是否应该允许推断元素类型? 例如:
Collection x = [1L, 2L];

// error CS1640: foreach statement cannot operate on variables of type 'Collection' because it implements multiple instantiations of 'IEnumerable<T>'; try casting to a specific interface instantiation
foreach (var x in new Collection) { }

static class Builder
{
    public Collection Create(ReadOnlySpan<long> items) => throw null;
}

[CollectionBuilder(...)]
class Collection : IEnumerable<int>, IEnumerable<string>
{
    IEnumerator<int> IEnumerable<int>.GetEnumerator() => throw null;
    IEnumerator<string> IEnumerable<string>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}
  • 创建并立即编制集合字面量索引是否合法? 注意:这需要回答以下未解决的问题,即集合字面量是否具有自然类型

  • 大型集合的堆栈分配可能会破坏堆栈。 编译器是否应该采用启发式方法将这些数据放在堆上? 是否应该不作具体说明,以便实现这种灵活性? 我们应该遵循 params Span<T> 的规范。

  • 我们是否需要将 spread_element 设为目标类型? 例如,请考虑:

    Span<int> span = [a, ..b ? [c] : [d, e], f];
    

    注意:这通常以以下形式出现,以允许在条件为真的情况下包含一组元素,或者如果条件不成立,则不包含任何内容:

    Span<int> span = [a, ..b ? [c, d, e] : [], f];
    

    为了计算此完整的字面量,我们需要计算其中的元素表达式。 这意味着能够计算 b ? [c] : [d, e]。 然而,如果没有一个目标类型来计算此表达式的上下文,也没有任何类型的自然类型,那么我们就无法确定如何处理这里的 [c][d, e]

    为了解决此问题,我们可以说,在评估字面值的 spread_element 表达式时,存在一个与字面值本身的目标类型等效的隐式目标类型。 因此,上面的内容将被重写为:

    int __e1 = a;
    Span<int> __s1 = b ? [c] : [d, e];
    int __e2 = f;
    
    Span<int> __result = stackalloc int[2 + __s1.Length];
    int __index = 0;
    
    __result[__index++] = a;
    foreach (int __t in __s1)
      __result[index++] = __t;
    __result[__index++] = f;
    
    Span<int> span = __result;
    

利用创建方法指定可构造的集合类型对转换分类的上下文非常敏感

在这种情况下,转换的存在取决于集合类型迭代类型概念。 如果存在一个 创建方法 接受 ReadOnlySpan<T>,而 T迭代类型,则转换是存在的。 否则,就不存在转换。

但是,迭代类型对执行 foreach 的上下文很敏感。 对于相同的集合类型,它可以根据范围中的扩展方法而有所不同,也可以是未定义的。

如果类型本身不是设计为可 foreach 的,那么对于 foreach 的目的来说,这样做效果还不错。 如果是这样,无论上下文是什么,扩展方法都不能改变类型被 foreach 的方式。

但是,这样的转换对上下文敏感,感觉有点奇怪。 实际上,转换过程是“不稳定”的。 一个显式设计为可构造集合类型被允许省略一个非常重要的细节定义,即其迭代类型。 这使得该类型本身“不可转换”。

下面是一个示例:

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

[CollectionBuilder(typeof(MyCollectionBuilder), nameof(MyCollectionBuilder.Create))]
class MyCollection
{
}
class MyCollectionBuilder
{
    public static MyCollection Create(ReadOnlySpan<long> items) => throw null;
    public static MyCollection Create(ReadOnlySpan<string> items) => throw null;
}

namespace Ns1
{
    static class Ext
    {
        public static IEnumerator<long> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                long s = l;
            }
        
            MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                               2];
        }
    }
}

namespace Ns2
{
    static class Ext
    {
        public static IEnumerator<string> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                string s = l;
            }
        
            MyCollection x1 = ["a",
                               2]; // error CS0029: Cannot implicitly convert type 'int' to 'string'
        }
    }
}

namespace Ns3
{
    class Program
    {
        static void Main()
        {
            // error CS1579: foreach statement cannot operate on variables of type 'MyCollection' because 'MyCollection' does not contain a public instance or extension definition for 'GetEnumerator'
            foreach (var l in new MyCollection())
            {
            }
        
            MyCollection x1 = ["a", 2]; // error CS9188: 'MyCollection' has a CollectionBuilderAttribute but no element type.
        }
    }
}

鉴于当前的设计,如果类型本身没有定义迭代类型本身,编译器就无法可靠地验证 CollectionBuilder 属性的应用。 如果我们不知道迭代类型,我们就不知道创建方法的签名应该是什么。 如果迭代类型来自上下文,就不能保证该类型总是在类似的上下文中使用。

参数集合 功能也受此影响。 在声明点无法可靠地预测 params 参数的元素类型,这让人感到很奇怪。 当前的提议还要求确保创建方法至少与params集合类型一样可访问。 除非集合类型本身定义其迭代类型,否则不可能以可靠的方式执行这种检查。

请注意,我们还为编译器打开了 https://github.com/dotnet/roslyn/issues/69676,它基本上描述了相同的问题,但从优化的角度进行讨论。

方案

需要使用 CollectionBuilder 属性的类型定义自己的迭代类型。 换句话说,这意味着该类型要么应该实现 IEnumarable/IEnumerable<T>,要么应该有具有正确签名的公共 GetEnumerator 方法(这不包括任何扩展方法)。

另外,现在要求创建方法“可在使用集合表达式的位置访问”。 这是基于可访问性的上下文依赖性的另一点。 此方法的目的与用户定义的转换方法的目的非常相似,而这种方法必须是公共的。 因此,我们应考虑要求将 创建方法 设为公开。

结束语

LDM-2024-01-08 修改后批准

迭代类型的概念在转换中的应用并不一致

  • 指向实现 System.Collections.Generic.IEnumerable<T>结构类类型,其中:
    • 对于每个元素Ei,都有到 T隐式转换

在这种情况下,似乎假设 T 必须是结构类类型迭代类型。 但是,该假设并不正确。 这可能会导致非常奇怪的行为。 例如:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public void Add(string l) => throw null;
    
    public IEnumerator<string> GetEnumerator() => throw null; 
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
        
        MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                           2];
        MyCollection x2 = new MyCollection() { "b" };
    }
}
  • 到实现 不实现 System.Collections.IEnumerable结构System.Collections.Generic.IEnumerable<T>

在实现过程中,我们似乎假定迭代类型object,但规范并没有指明这一点,只是不要求每个元素转换为任何内容。 一般来说,型的迭代类型 不一定是 object 类型。 从下面的例子中可以看出这一点:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    public IEnumerator<string> GetEnumerator() => throw null; 
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
    }
}

迭代类型的概念是参数集合功能的基础。 而这个问题导致了两种功能之间的奇怪差异。 例如:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 

    public void Add(long l) => throw null; 
    public void Add(string l) => throw null; 
}

class Program
{
    static void Main()
    {
        Test("2"); // error CS0029: Cannot implicitly convert type 'string' to 'long'
        Test(["2"]); // error CS1503: Argument 1: cannot convert from 'collection expressions' to 'string'
        Test(3); // error CS1503: Argument 1: cannot convert from 'int' to 'string'
        Test([3]); // Ok

        MyCollection x1 = ["2"]; // error CS0029: Cannot implicitly convert type 'string' to 'long'
        MyCollection x2 = [3];
    }

    static void Test(params MyCollection a)
    {
    }
}
using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 
    public void Add(object l) => throw null;
}

class Program
{
    static void Main()
    {
        Test("2", 3); // error CS1503: Argument 2: cannot convert from 'int' to 'string'
        Test(["2", 3]); // Ok
    }

    static void Test(params MyCollection a)
    {
    }
}

选择一种方式进行对齐可能会更好。

方案

指定实现 System.Collections.Generic.IEnumerable<T>System.Collections.IEnumerable结构类类型迭代类型方面的可转换性,并要求每个元素Ei迭代类型进行隐式转换

结束语

已审核 LDM-2024-01-08

集合表达式转换是否应该要求提供一套用于构建的最基本的 API?

根据转换可构造集合类型实际上可能是不可构造的,这很可能导致一些意想不到的重载决策行为。 例如:

class C1
{
    public static void M1(string x)
    {
    }
    public static void M1(char[] x)
    {
    }
    
    void Test()
    {
        M1(['a', 'b']); // error CS0121: The call is ambiguous between the following methods or properties: 'C1.M1(string)' and 'C1.M1(char[])'
    }
}

但是,“C1.M1(string)”并不是一个可以使用的候选项,因为:

error CS1729: 'string' does not contain a constructor that takes 0 arguments
error CS1061: 'string' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)

下面是另一个示例,其中有一个用户定义的类型和一个更严重的错误,甚至没有提到一个有效的候选选项:

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(C1 x)
    {
    }
    public static void M1(char[] x)
    {
    }

    void Test()
    {
        M1(['a', 'b']); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
    }

    public static implicit operator char[](C1 x) => throw null;
    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

这种情况似乎与我们以前将方法组转换为委托的情况非常相似。 也就是说,在某些情况下,转换是存在的,但却是错误的。 我们决定改进这种情况,确保如果转换是错误的,那么它就不存在。

请注意,使用“参数集合”功能时,我们可能会遇到类似的问题。 对于不可构造的集合,禁止使用 params 修饰符也许是个好办法。 但是,在目前的建议中,这种检查是基于转换部分。 下面是一个示例:

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(params C1 x) // It is probably better to report an error about an invalid `params` modifier
    {
    }
    public static void M1(params ushort[] x)
    {
    }

    void Test()
    {
        M1('a', 'b'); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
        M2('a', 'b'); // Ok
    }

    public static void M2(params ushort[] x)
    {
    }

    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

看起来这个问题之前已经讨论过一些,请参阅https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions。 当时,有人提出一个论点,即当前的规则与内插字符串处理程序的指定方式是一致的。 下面是一段引用:

具体而言,插值字符串处理程序最初就是这样规定的,但我们在考虑了这个问题后对规范进行了修订。

虽然有一些相似之处,但也有值得考虑的重要区别。 以下是引自 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion 的内容:

如果类型 T 的属性为 ,则称该类型为 System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute。 存在一个从 interpolated_string_expressionT 的隐式 interpolated_string_handler_conversion,或一个完全由 _interpolated_string_expression_s 组成并仅使用 运算符的 +

目标类型必须有一个特殊属性,该属性是作者希望该类型成为插值字符串处理程序的一个有力指标。 可以认为,该属性的出现并非巧合。 相反,仅仅因为一个类型是“可枚举的”,并不必然意味着作者有意图让该类型是可构造的。 然而,创建方法的存在(在[CollectionBuilder(...)]上用 属性表示),感觉像是作者希望该类型可被构造的一个强有力的指标。

方案

对于实现了 结构System.Collections.IEnumerable,如果没有创建方法转换部分至少需要存在以下 API:

  • 一个没有参数的可访问构造函数。
  • 一个可访问的 Add 实例或扩展方法,可以调用迭代类型的值作为参数。

为了实现 Params Collections 功能,当这些 API 被声明为公共并且是实例方法(相对于扩展方法)时,这些类型是有效的 params 类型。

结束语

LDM-2024-01-10 修改后批准

设计会议

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-01.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-09.md#ambiguity-of--in-collection-expressions https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-28.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-08.md https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-10.md

工作组会议

https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-06.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-14.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-21.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-05.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-28.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-05-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-12.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-03.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-10.md

即将到来的议程事项

  • 大型集合的堆栈分配可能会破坏堆栈。 编译器是否应该采用启发式方法将这些数据放在堆上? 是否应该不作具体说明,以便实现这种灵活性? 我们应该遵循规范/实现对 params Span<T> 所做的规定。 选项包括:

    • 始终 stackalloc。 教导人们在使用 Span 时要小心。 这允许像 Span<T> span = [1, 2, ..s] 这样的功能正常运作,并且只要 s 的值很小,就不会出现问题。 如果这可能会影响堆栈,用户可以创建一个数组来代替,然后获得一个范围。 这似乎最符合人们的愿望,但也有极大的危险性。
    • 只有当字面量具有固定数量的元素(即没有扩展元素)时,才使用 stackalloc。 这样可能会使系统始终处于安全状态,使用固定的堆栈方式,并希望编译器能够重复利用那个固定缓冲区。 然而,这意味着像 [1, 2, ..s] 这样的事情永远不可能发生,即使用户在运行时知道它是完全安全的。
  • 重载决策的工作原理是什么? 如果 API 具有:

    public void M(T[] values);
    public void M(List<T> values);
    

    M([1, 2, 3]) 会发生什么? 我们可能需要为这些转换定义“优越性”。

  • 我们应该扩展集合初始化器来查找非常常见的 AddRange 方法吗? 基础构造类型可以使用它来更有效地执行扩展元素的添加。 我们可能还希望寻找类似 .CopyTo 的内容。 与直接在转换代码中进行枚举相比,这些方法最终可能会导致过多的分配/调度,因此可能会存在一些弊端。

  • 应更新泛型类型推理,以便将类型信息流向/传出集合字面量。 例如:

    void M<T>(T[] values);
    M([1, 2, 3]);
    

    推理算法自然也会意识到这一点。 一旦“基本”可构造集合类型的情况(T[]I<T>Span<T>new T())支持了这一点,那么 Collect(constructible_type) 的情况也应该支持。 例如:

    void M<T>(ImmutableArray<T> values);
    M([1, 2, 3]);
    

    此处,Immutable<T> 可通过 init void Construct(T[] values) 方法来构造。 因此,T[] values 类型将针对 [1, 2, 3] 使用推断,从而导致 intT 的推断。

  • 强制转换/索引歧义。

    今天,以下表达式将被索引到

    var v = (Expr)[1, 2, 3];
    

    但是,可以做一些像这样的事情会很好:

    var v = (ImmutableArray<int>)[1, 2, 3];
    

    我们可以/应该在这里休息一下吗?

  • ?[相关的语法歧义。

    或许应该更改 nullable index access 的规则,规定 ?[之间不能有空格。 这将是一个重大变更(但可能影响较小,因为如果你用空格输入它们的话,VS 已经强制将它们合并在一起)。 如果我们这样做,那么我们可以使 x?[y] 的解析方式与 x ? [y]不同。

    如果我们想选择 https://github.com/dotnet/csharplang/issues/2926,会出现类似的情况。 在那个世界中,x?.yx ? .y之间的关系是模糊的。 如果要求 ?. 相邻,我们就可以在句法上将这两种情况区分开来。