更好地从集合表达式元素进行转换

注意

本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。

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

可以在 规范一文中详细了解将功能规范采用 C# 语言标准的过程。

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

总结

更新更好的转换规则,以与 params更加一致,并更好地处理当前的模糊场景。 例如,ReadOnlySpan<string>ReadOnlySpan<object> 在当前 [""]的重载解析过程中可能会导致歧义。

详细设计

以下是较好的表达式转换规则。 这些规则将替换 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md#overload-resolution中的规则。

这些规则包括:

给定隐式转换 C₁,它将表达式 E 转换为类型 T₁,以及隐式转换 C₂,它将表达式 E 转换为类型 T₂C₁C₂ 更好的转换,如果满足以下任一条件:

  • E集合表达式C₁ 是比 C₂ 表达式更优的集合转换
  • E 不是 集合表达式,并且以下任一条件成立:
    • E 完全匹配 T₁ET₂ 不完全匹配
    • E 要么与 T₁T₂完全匹配,要么都不匹配,而 T₁ 是比 T₂ 更好的 转换目标
  • E 是方法组...

我们为 添加一个新定义,以便更好地将表达式转换成集合,如下所示:

假定为:

  • E 是一个包含元素表达式 [EL₁, EL₂, ..., ELₙ] 的集合表达式
  • T₁T₂ 是集合类型
  • E₁T₁ 的元素类型
  • E₂T₂ 的元素类型
  • CE₁ᵢ 是从 ELᵢE₁ 的一系列转换
  • CE₂ᵢ 是从 ELᵢE₂ 的一系列转换

如果存在从 E₁E₂的标识转换,则元素之间的转换效果同样出色。 否则,到 E₁ 的元素转换 优于元素转换E₂(如果:

  • 对于每个 ELᵢCE₁ᵢ 至少和 CE₂ᵢ一样好,而且
  • 至少有一个 i,其中 CE₁ᵢCE₂ᵢ 更好。否则,两组元素转换都不比另一组更好,它们也不如彼此一样好。
    如果 ELᵢ 不是一个 spread 元素,则使用更好的表达式转换进行转换比较。 如果 ELᵢ 是展开元素,我们可以分别更加有效地将展开集合的元素类型转换为 E₁E₂

C₁ 是一种表达式更好的集合转换(相较于 C₂),但前提是:

  • T₁T₂ 都不是 跨度类型T₁ 可隐式转换为 T₂,而 T₂ 不能隐式转换为 T₁,或者...
  • E₁ 没有到 E₂ 的标识转换,并且到 E₁ 的元素转换优于E₂的元素转换,或者
  • E₁E₂ 进行标识转换,且以下条件之一成立:
    • T₁System.ReadOnlySpan<E₁>T₂System.Span<E₂>,或者
    • T₁System.ReadOnlySpan<E₁>System.Span<E₁>,且 T₂ 是具有元素类型E₂array_or_array_interface

否则,两个集合类型都不更好,结果不明确。

注意

这些规则意味着,对于空集合表达式而言,公开了使用不同元素类型的重载且未在集合类型之间进行转换的方法是含糊不清的。 例如:

public void M(ReadOnlySpan<int> ros) { ... }
public void M(Span<int?> span) { ... }

M([]); // Ambiguous

场景:

在纯英语中,集合类型本身必须相同, 或明确更好的(即,List<T>List<T> 是相同的,List<T> 明显优于 IEnumerable<T>,并且无法比较 List<T>HashSet<T>),更好的集合类型的元素转换也必须相同或更好(即,我们不能在 ReadOnlySpan<object>Span<string> 之间决定 [""], 用户需要做出该决定。 更多示例包括:

T₁ T₂ E C₁ 转换 C₂ 转换 CE₁ᵢCE₂ᵢ 结果
List<int> List<byte> [1, 2, 3] [Identity, Identity, Identity] [Implicit Constant, Implicit Constant, Implicit Constant] CE₁ᵢ 更好 List<int> 已选取
List<int> List<byte> [(int)1, (byte)2] [Identity, Implicit Numeric] 不適用 T₂ 不适用 List<int> 已选取
List<int> List<byte> [1, (byte)2] [Identity, Implicit Numeric] [Implicit Constant, Identity] 两者都不更好 模糊
List<int> List<byte> [(byte)1, (byte)2] [Implicit Numeric, Implicit Numeric] [Identity, Identity] CE₂ᵢ 更好 List<byte> 已选取
List<int?> List<long> [1, 2, 3] [Implicit Nullable, Implicit Nullable, Implicit Nullable] [Implicit Numeric, Implicit Numeric, Implicit Numeric] 两者都不更好 模糊
List<int?> List<ulong> [1, 2, 3] [Implicit Nullable, Implicit Nullable, Implicit Nullable] [Implicit Numeric, Implicit Numeric, Implicit Numeric] CE₁ᵢ 更好 List<int?> 已选取
List<short> List<long> [1, 2, 3] [Implicit Numeric, Implicit Numeric, Implicit Numeric] [Implicit Numeric, Implicit Numeric, Implicit Numeric] CE₁ᵢ 更好 List<short> 已选取
IEnumerable<int> List<byte> [1, 2, 3] [Identity, Identity, Identity] [Implicit Constant, Implicit Constant, Implicit Constant] CE₁ᵢ 更好 IEnumerable<int> 已选取
IEnumerable<int> List<byte> [(byte)1, (byte)2] [Implicit Numeric, Implicit Numeric] [Identity, Identity] CE₂ᵢ 更好 List<byte> 已选取
int[] List<byte> [1, 2, 3] [Identity, Identity, Identity] [Implicit Constant, Implicit Constant, Implicit Constant] CE₁ᵢ 更好 int[] 已选取
ReadOnlySpan<string> ReadOnlySpan<object> ["", "", ""] [Identity, Identity, Identity] [Implicit Reference, Implicit Reference, Implicit Reference] CE₁ᵢ 更好 ReadOnlySpan<string> 已选取
ReadOnlySpan<string> ReadOnlySpan<object> ["", new object()] 不適用 [Implicit Reference, Identity] T₁ 不适用 ReadOnlySpan<object> 已选取
ReadOnlySpan<object> Span<string> ["", ""] [Implicit Reference] [Identity] CE₂ᵢ 更好 Span<string> 已选取
ReadOnlySpan<object> Span<string> [new object()] [Identity] 不適用 T₁ 不适用 ReadOnlySpan<object> 已选取
ReadOnlySpan<InterpolatedStringHandler> ReadOnlySpan<string> [$"{1}"] [Interpolated String Handler] [Identity] CE₁ᵢ 更好 ReadOnlySpan<InterpolatedStringHandler> 已选取
ReadOnlySpan<InterpolatedStringHandler> ReadOnlySpan<string> [$"{"blah"}"] [Interpolated String Handler] [Identity] - 但常量 CE₂ᵢ 更好 ReadOnlySpan<string> 已选取
ReadOnlySpan<string> ReadOnlySpan<FormattableString> [$"{1}"] [Identity] [Interpolated String] CE₂ᵢ 更好 ReadOnlySpan<string> 已选取
ReadOnlySpan<string> ReadOnlySpan<FormattableString> [$"{1}", (FormattableString)null] 不適用 [Interpolated String, Identity] T₁ 不适用 ReadOnlySpan<FormattableString> 已选取
HashSet<short> Span<long> [1, 2] [Implicit Constant, Implicit Constant] [Implicit Numeric, Implicit Numeric] CE₁ᵢ 更好 HashSet<short> 已选取
HashSet<long> Span<short> [1, 2] [Implicit Numeric, Implicit Numeric] [Implicit Constant, Implicit Constant] CE₂ᵢ 更好 Span<short> 已选取

开放性问题

我们应在多大程度上优先考虑 ReadOnlySpan/Span 而非其他类型?

按照现在的规定,下列重载将是模棱两可的:

C.M1(["Hello world"]); // Ambiguous, no tiebreak between ROS and List
C.M2(["Hello world"]); // Ambiguous, no tiebreak between Span and List

C.M3(["Hello world"]); // Ambiguous, no tiebreak between ROS and MyList.

C.M4(["Hello", "Hello"]); // Ambiguous, no tiebreak between ROS and HashSet. Created collections have different contents

class C
{
    public static void M1(ReadOnlySpan<string> ros) {}
    public static void M1(List<string> list) {}

    public static void M2(Span<string> ros) {}
    public static void M2(List<string> list) {}

    public static void M3(ReadOnlySpan<string> ros) {}
    public static void M3(MyList<string> list) {}

    public static void M4(ReadOnlySpan<string> ros) {}
    public static void M4(HashSet<string> hashset) {}
}

class MyList<T> : List<T> {}

我们打算走多远? List<T> 变体似乎很合理,List<T> 的子类型存在大量。 但是,HashSet 版本具有截然不同的语义,我们到底有多确定它在这个 API 中比 ReadOnlySpan 更糟糕呢?