第一类 Span 类型

注释

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

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

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

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

概要

我们为Span<T>ReadOnlySpan<T>在语言中的一流支持进行了引入,包括新的隐式转换类型,并在更多地方考虑它们,从而使对这些整数类型的编程更加自然。

动机

自从在 C# 7.2 中引入之后,Span<T>ReadOnlySpan<T> 已经以多种关键方式融入到语言和基类库(BCL)中。 开发人员将从中大受裨益,因为这些改进提高了性能,同时不影响开发人员的安全性。 但是,该语言在几个关键方面与这些类型保持距离,这使得很难表达 API 的意图,并导致新的 API 在表面积上大量重复。 例如,BCL 在 .NET 9 中添加了许多新的张量基元 API,但这些 API 都是在ReadOnlySpan<T>中提供的。 C# 无法识别 ReadOnlySpan<T>Span<T>T[] 之间的关系。因此,即使这些类型之间存在用户定义的转换,它们也不能用于扩展方法接收器,也无法与其他用户定义的转换组合,并且对所有泛型类型推断方案无益。 用户需要使用显式转换或类型参数,这意味着 IDE 工具不会引导用户使用这些 API,因为不会向 IDE 指示在转换后传递这些类型是有效的。 为了为此样式的 API 提供最大可用性,BCL 必须定义一套完整的 Span<T>T[] 重载,而这需要维护大量重复的外围应用,而实际却并无收益。 此建议旨在通过让语言更直接地识别这些类型和转换来解决该问题。

例如,BCL 只能为任意 MemoryExtensions 辅助函数添加一个重载,例如:

int[] arr = [1, 2, 3];
Console.WriteLine(
    arr.StartsWith(1) // CS8773 in C# 13, permitted with this proposal
    );

public static class MemoryExtensions
{
    public static bool StartsWith<T>(this ReadOnlySpan<T> span, T value) where T : IEquatable<T> => span.Length != 0 && EqualityComparer<T>.Default.Equals(span[0], value);
}

以前,需要 Span 和数组重载才能使扩展方法可用于 Span/数组类的变量,因为用户定义的转换(存在于 Span/数组/ReadOnlySpan 之间)不会考虑用于扩展接收器。

详细设计

此提案中的更改将绑定到 LangVersion >= 14

Span 转换

我们在§10.2.1中向列表添加了一种新类型的隐式转换,即隐式跨度转换。 此转换为一个类型转换,其定义如下:


隐式跨度转换允许array_typesSystem.Span<T>、和System.ReadOnlySpan<T>string相互转换,如下所示:

  • 从任意元素类型为 array_type 的单维 Ei 转换为 System.Span<Ei>
  • 从任何带有元素类型array_type的单维Ei转换到System.ReadOnlySpan<Ui>,前提是Ei可以协变地转换(§18.2.3.3)到Ui
  • System.Span<Ti>System.ReadOnlySpan<Ui>,前提是Ti可以协变转换(§18.2.3.3)至Ui
  • System.ReadOnlySpan<Ti>System.ReadOnlySpan<Ui>,前提是Ti可以协变转换(§18.2.3.3)至Ui
  • stringSystem.ReadOnlySpan<char>

如果任意 Span/ReadOnlySpan 类型均为 ref struct,并且它们可通过完全限定的名称 (LDM 2024-06-24) 进行匹配,那么这些类型就被视为适用于该转换。

我们还将 隐式跨度转换添加到标准隐式转换 列表(§10.4.2)。 这允许重载解析在执行参数解析时考虑它们,如之前链接的 API 建议所示。

显式 Span 转换如下:

  • 所有隐式 Span 转换
  • 从元素类型为 Ti 转换为 System.Span<Ui>System.ReadOnlySpan<Ui>,但前提是存在从 TiUi 的显式引用转换。

与其他标准显式转换 (§10.4.3)不同,不存在标准显式 span 转换,而对于其他标准显式转换而言,只要存在相反的标准隐式转换,它们就始终存在。

用户定义的转换

在隐式或显式跨度转换存在的类型之间进行转换时,不考虑用户定义的转换。

隐式跨度转换被豁免于以下规则:如果存在非用户定义的转换,则无法在类型之间定义用户定义的运算符(§10.5.2 允许的用户定义转换)。 这是必要的功能,因此即使在切换到 C# 14 之后 BCL 仍可继续定义现有的 Span 转换运算符(仍需将这些运算符用于较低的 LangVersion,同时还因为这些运算符会用于新标准 Span 转换的代码生成)。 但可以将其视为实现详细信息(codegen 和 lower LangVersions 不是规范的一部分),Roslyn 无论如何都违反了规范的这一部分(不强制实施有关用户定义的转换的这一特定规则)。

扩展接收器

我们还在确定适用性(12.8.9.3)时,将扩展方法第一个参数的隐式跨度转换添加到可接受的隐式转换列表(以粗体显示的更改):

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

  • Cᵢ 是非泛型、非嵌套类
  • Mₑ 的名称是 标识符
  • Mₑ 是可访问的,当作为静态方法应用于参数时适用,如上所示
  • 存在从 expr 的第一个参数的类型的隐式标识、引用或装箱Mₑ 转换。 在对方法组转换进行重载解析时,不考虑跨度转换。

请注意,方法组转换(LDM 2024-07-15)中的扩展接收器不考虑隐式跨度转换,这会使以下代码继续工作,而不是导致编译时错误 CS1113: Extension method 'E.M<int>(Span<int>, int)' defined on value type 'Span<int>' cannot be used to create delegates

using System;
using System.Collections.Generic;
Action<int> a = new int[0].M; // binds to M<int>(IEnumerable<int>, int)
static class E
{
    public static void M<T>(this Span<T> s, T x) => Console.Write(1);
    public static void M<T>(this IEnumerable<T> e, T x) => Console.Write(2);
}

作为潜在的未来工作,我们可能会考虑去除在方法组转换中不为扩展接收方考虑 Span 转换的这一情况,而改为实施更改,以便类似上述情况的场景最终能成功调用 Span 重载:

  • 编译器可发出一个 thunk 函数,而它会将数组作为接收方,并在其中执行 Span 转换(类似于用户手动创建 x => new int[0].M(x) 一类的委托)。
  • 如果值委托被实现,则可直接将 Span 作为接收方。

方差

隐式跨度转换方差部分的目标是复制一定数量的协变System.ReadOnlySpan<T>。 为在此完全实现泛型中的变型,需要进行运行时更改(请参阅https://github.com/dotnet/csharplang/blob/main/proposals/csharp-13.0/ref-struct-interfaces.md中关于ref struct类型在泛型中使用的说明),但我们可以通过使用建议的 .NET 9 API: https://github.com/dotnet/runtime/issues/96952,来允许有限的协变。 此举可让该语言在某些情况下将 System.ReadOnlySpan<T> 视为 T 已被声明为 out T。 但是,我们不会深入研究通过所有差异场景转换此变体,也不会将其添加到 §18.2.3.3 内可转换差异的定义中。 如果在将来,我们将此运行时更改为更深入地了解此处的差异,则可采取小型中断性变更以在该语言中完全识别它。

模式

请注意,当 ref struct 用作任何模式中的类型时,仅允许身份转换:

class C<T> where T : allows ref struct
{
    void M1(T t) { if (t is T x) { } } // ok (T is T)
    void M2(R r) { if (r is R x) { } } // ok (R is R)
    void M3(T t) { if (t is R x) { } } // error (T is R)
    void M4(R r) { if (r is T x) { } } // error (R is T)
}
ref struct R { }

的规范中,is-type 运算符 (§12.12.12.1):

操作 E is T [...] 的结果是一个布尔值,表示 E 是否为非空,以及能否通过引用转换、装箱转换、取消装箱转换、包装转换或取消包装转换成功转换为类型 T

[...]

如果 T 是不可为 null 的值类型,则如果 trueD 的类型相同,则结果 T

此行为不会随此功能而更改,因此无法编写模式 Span/ReadOnlySpan,尽管数组可以编写类似的模式(包括方差):

using System;

M1<object[]>(["0"]); // prints
M1<string[]>(["1"]); // prints

void M1<T>(T t)
{
    if (t is object[] r) Console.WriteLine(r[0]); // ok
}

void M2<T>(T t) where T : allows ref struct
{
    if (t is ReadOnlySpan<object> r) Console.WriteLine(r[0]); // error
}

代码生成

无论用于实现它们的运行时帮助程序是否存在(LDM 2024-05-13),转换将始终存在。 如果帮助程序不存在,尝试使用转换时将导致编译时错误,因为缺少编译器所需的成员。

编译器需要使用以下帮助程序或等效项来实现转换:

转换 助手
数组到 Span static implicit operator Span<T>(T[])(在 Span<T> 中定义)
数组到 ReadOnlySpan static implicit operator ReadOnlySpan<T>(T[])(在 ReadOnlySpan<T> 中定义)
Span 到 ReadOnlySpan static implicit operator ReadOnlySpan<T>(Span<T>)(在 Span<T> 中定义)和 static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
ReadOnlySpan 到 ReadOnlySpan static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
字符串到 ReadOnlySpan static ReadOnlySpan<char> MemoryExtensions.AsSpan(string)

请注意,MemoryExtensions.AsSpan 被使用,而不是使用在 string 上定义的等效隐式运算符。 这意味着不同 LangVersions 的代码生成有所不同(在 C# 13 中使用隐式运算符,在 C# 14 中使用静态方法 AsSpan)。 另一方面,可以在 .NET Framework 上发出转换( AsSpan 方法存在,而 string 运算符不存在)。

显式数组到 (ReadOnly)Span 的转换首先将源数组显式转换为具有目标元素类型的数组,然后通过与隐式转换相同的辅助程序(即相应的 op_Implicit(T[]))转换为 (ReadOnly)Span。

更好的从表达式转换

优化的表达式转换§12.6.4.5)已经更新为优先选择隐式跨度转换。 它基于集合表达式重载解析更改

给定从表达式 C₁ 转换为类型 E 的隐式转换 T₁ 和从表达式 C₂ 转换为类型 E 的隐式转换 T₂,如果以下条件之一成立,则 C₁ 是比 C₂

  • E 是一个集合表达式,而 C₁ 是比
  • E 不是 集合表达式,并且满足以下条件之一:
    • E 完全匹配 T₁ET₂ 不完全匹配
    • ET₁T₂ 都不完全匹配,C₁ 是隐式跨度转换,而 C₂ 不是隐式跨度转换
    • ET₁T₂ 同时完全匹配或与二者均不匹配,C₁C₂ 均不是隐式 Span 转换,同时 T₁ 是优于 T₂
  • E 是一个方法组, T₁ 与方法组中用于转换 C₁的单个最佳方法兼容,并且 T₂ 与方法组中用于转换的单个最佳方法不兼容 C₂

更好的转换目标

更好的转换目标§12.6.4.7)已更新为优先选择ReadOnlySpan<T>而不是Span<T>

给定两种类型 T₁T₂,如果满足以下条件之一,则 T₁是比

  • T₁System.ReadOnlySpan<E₁>T₂System.Span<E₂>,且存在从 E₁E₂ 的标识变换
  • T₁System.ReadOnlySpan<E₁>T₂System.ReadOnlySpan<E₂>,存在从T₁T₂的隐式转换,但不存在从T₂T₁的隐式转换
  • T₁
  • ...

设计会议:

优先注释

更好的表达式转换规则应确保每当由于新的 Span 转换而使重载变为适用时,由于首选新适用的重载,从而避免与另一重载存在任何潜在的歧义。

如果没有这条规则,以下在 C# 13 中成功编译的代码,在 C# 14 中会由于从数组到适用于扩展方法接收者的 ReadOnlySpan 的新的标准隐式转换,而导致出现歧义错误。

using System;
using System.Collections.Generic;

var a = new int[] { 1, 2, 3 };
a.M();

static class E
{
    public static void M(this IEnumerable<int> x) { }
    public static void M(this ReadOnlySpan<int> x) { }
}

该规则还允许引入之前会导致歧义的新 API,例如:

using System;
using System.Collections.Generic;

C.M(new int[] { 1, 2, 3 }); // would be ambiguous before

static class C
{
    public static void M(IEnumerable<int> x) { }
    public static void M(ReadOnlySpan<int> x) { } // can be added now
}

警告

由于针对仅存在于 LangVersion >= 14 中的 span 转换定义了优先规则,因此,如果 API 作者希望继续支持 LangVersion <= 13 上的用户,则无法添加此类新的重载。 例如,如果 .NET 9 BCL 引入了此类重载,则升级到 net9.0 TFM 但保持较低的 LangVersion 的用户将遇到现有代码的歧义错误。 另 请参阅下面的一个公开问题

类型推理

我们将对规范中的类型推断部分进行更新,具体更改如下所示(以粗体展示)。

12.6.3.9 精确推理

类型 U类型 V如下:

  • 如果 是未固定的 之一,则 会被添加到 的确切边界集。
  • 否则,集 V₁...VₑU₁...Uₑ 将通过检查是否存在以下情况来确定:
    • V 是数组类型 V₁[...]U 是相同排名的数组类型 U₁[...]
    • VSpan<V₁>U是一个数组类型U₁[]Span<U₁>
    • VReadOnlySpan<V₁>,而U是数组类型U₁[]Span<U₁>ReadOnlySpan<U₁>
    • V 是类型 V₁?U 是类型 U₁
    • V 是构造类型 C<V₁...Vₑ>U 是构造类型 C<U₁...Uₑ>
      如果出现上述任何一种情况,那么就会从每个 到相应的 Uᵢ 进行Vᵢ
  • 否则,不进行推理。

12.6.3.10 下限推理

从类型U类型 V如下:

  • 如果 是未固定之一,则将 添加到 的下限集。
  • 否则,如果 V 是类型 V₁?U 是类型 U₁?,则从 U₁V₁进行下限推理。
  • 否则,集 U₁...UₑV₁...Vₑ 将通过检查是否存在以下情况来确定:
    • V是数组类型V₁[...]U是同一排名的数组类型U₁[...]
    • VSpan<V₁>U是一个数组类型U₁[]Span<U₁>
    • VReadOnlySpan<V₁>,而U是数组类型U₁[]Span<U₁>ReadOnlySpan<U₁>
    • VIEnumerable<V₁>ICollection<V₁>IReadOnlyList<V₁>>IReadOnlyCollection<V₁>IList<V₁> 之一,U 是单维数组类型 U₁[]
    • V 是一个构造的 classstructinterfacedelegate 类型 C<V₁...Vₑ>,并且存在一个唯一的类型 C<U₁...Uₑ>,使得 U(或者,如果 U 是一个类型 parameter,则其有效基类或其有效接口集的任何成员)与之相同,inherits 自(直接或间接)或实现(直接或间接)C<U₁...Uₑ>
    • (“唯一性”限制意味着,在接口 C<T>{} class U: C<X>, C<Y>{}的情况下,在从 U 推断到 C<T> 时不会进行推理,因为 U₁ 可能是 XY
      如果这些情况中的任何一种适用,则从每个 Uᵢ 到相应的 Vᵢ 进行推理,如下所示:
    • 如果不知道 Uᵢ 是引用类型,则会进行确切推定
    • 否则,如果U是数组类型,则进行下限推理推理取决于V的类型
      • 如果VSpan<Vᵢ>,则进行确切推理
      • 如果V为数组类型或数组ReadOnlySpan<Vᵢ>类型,则进行下限推理
    • 否则,如果 USpan<Uᵢ>,则推断取决于 V 的类型。
      • 如果VSpan<Vᵢ>,则进行确切推理
      • 如果VReadOnlySpan<Vᵢ>,则进行下限推理
    • 否则,如果UReadOnlySpan<Uᵢ>VReadOnlySpan<Vᵢ>,则进行下限推理
    • 否则,如果 VC<V₁...Vₑ>,那么推理依赖于 i-thC 类型参数:
      • 如果是协变的,那么会进行 下限推理
      • 如果它是逆变类型的,则会进行上限推定
      • 如果它是固定的,则会进行确切推定
  • 否则,不进行推理。

没有上限推理规则,因为无法命中它们。 类型推理从不从上限开始,其过程必须经过下限推理和逆变类型参数。 由于规则“如果 Uᵢ 未知为引用类型,则进行 确切推理 ”,源类型参数不能 Span/ReadOnlySpan (不能为引用类型)。 但是,上限范围推理仅在源类型为 a Span/ReadOnlySpan时适用,因为它的规则如下:

  • USpan<U₁>V是一个数组类型V₁[]Span<V₁>
  • UReadOnlySpan<U₁>,而V是数组类型V₁[]Span<V₁>ReadOnlySpan<V₁>

中断性变更

和任何会改变现有场景的转换方式的提案一样,该提案也会引入一些新的重大变化。 下面是几个示例:

对数组调用 Reverse

调用 x.Reverse()(其中 x 是类型 T[] 的实例)以前会绑定到 IEnumerable<T> Enumerable.Reverse<T>(this IEnumerable<T>),而现在会绑定到 void MemoryExtensions.Reverse<T>(this Span<T>)。 遗憾的是,这些 API 不兼容(后者会就地逆转并返回 void)。

.NET 10 通过添加特定于数组的重载 IEnumerable<T> Reverse<T>(this T[])来缓解此问题,请参阅 https://github.com/dotnet/runtime/issues/107723

void M(int[] a)
{
    foreach (var x in a.Reverse()) { } // fine previously, an error now (`Reverse` returns `void`)
    foreach (var x in Enumerable.Reverse(a)) { } // workaround
}

另请参阅:

设计会议:https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#reverse

歧义

以下示例在 Span 重载的类型推理中曾失败过,但现在从数组到 Span 的类型推理已成功,因此这些示例存在歧义。 若要解决此问题,用户可以使用 .AsSpan() 或 API 作者可以使用 OverloadResolutionPriorityAttribute

var x = new long[] { 1 };
Assert.Equal([2], x); // previously Assert.Equal<T>(T[], T[]), now ambiguous with Assert.Equal<T>(ReadOnlySpan<T>, Span<T>)
Assert.Equal([2], x.AsSpan()); // workaround
var x = new int[] { 1, 2 };
var s = new ArraySegment<int>(x, 1, 1);
Assert.Equal(x, s); // previously Assert.Equal<T>(T, T), now ambiguous with Assert.Equal<T>(Span<T>, Span<T>)
Assert.Equal(x.AsSpan(), s); // workaround

xUnit 正在添加更多重载来缓解此问题: https://github.com/xunit/xunit/discussions/3021

设计会议:https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#new-ambiguities

协变数组

采用 IEnumerable<T> 的重载适用于协变数组,但采用 Span<T> 的重载并不适用(而我们现在会优先使用它们),因为 Span 转换会对协变数组抛出 ArrayTypeMismatchException。 可以说,Span<T> 重载不应存在,而应取 ReadOnlySpan<T>。 为解决此问题,用户可使用 .AsEnumerable(),或者 API 作者可使用 OverloadResolutionPriorityAttribute 或添加首选的 ReadOnlySpan<T> 重载(由于存在优先规则)。

string[] s = new[] { "a" };
object[] o = s;

C.R(o); // wrote 1 previously, now crashes in Span<T> constructor with ArrayTypeMismatchException
C.R(o.AsEnumerable()); // workaround

static class C
{
    public static void R<T>(IEnumerable<T> e) => Console.Write(1);
    public static void R<T>(Span<T> s) => Console.Write(2);
    // another workaround:
    public static void R<T>(ReadOnlySpan<T> s) => Console.Write(3);
}

设计会议:https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#covariant-arrays

首选 ReadOnlySpan 而不是 Span

优先规则导致 ReadOnlySpan 重载优先于 Span 重载,以避免在ArrayTypeMismatchException中出现 。 在某些情况下,这可能会导致编译中断,例如,当重载因返回类型而异时:

double[] x = new double[0];
Span<ulong> y = MemoryMarshal.Cast<double, ulong>(x); // previously worked, now a compilation error (returns ReadOnlySpan, not Span)
Span<ulong> z = MemoryMarshal.Cast<double, ulong>(x.AsSpan()); // workaround

static class MemoryMarshal
{
    public static ReadOnlySpan<TTo> Cast<TFrom, TTo>(ReadOnlySpan<TFrom> span) => default;
    public static Span<TTo> Cast<TFrom, TTo>(Span<TFrom> span) => default;
}

请参阅 https://github.com/dotnet/roslyn/issues/76443

表达式树

接受 Span(如 MemoryExtensions.Contains)的重载优先于经典重载(如 Enumerable.Contains),且即使在表达式树中也是如此,但解释器引擎不支持 ref 结构。

Expression<Func<int[], int, bool>> exp = (array, num) => array.Contains(num);
exp.Compile(preferInterpretation: true); // fails at runtime in C# 14

Expression<Func<int[], int, bool>> exp2 = (array, num) => Enumerable.Contains(array, num); // workaround
exp2.Compile(preferInterpretation: true); // ok

同样,像 LINQ-to-SQL 这样的翻译引擎,如果它们的树访问器预期 Enumerable.Contains 而实际遇到的是 MemoryExtensions.Contains,则需要对此作出反应。

另请参阅:

设计会议:

通过继承实现的用户定义的转换

通过将隐式跨度转换添加到 标准隐式转换 列表,我们可以在类型层次结构中涉及用户定义的转换时更改行为。 此示例显示了此更改,以便与已与新的 C# 14 行为表现相同的整数情况进行区分。

Span<string> span = [];
var d = new Derived();
d.M(span); // Base today, Derived tomorrow
int i = 1;
d.M(i); // Derived today, demonstrates new behavior

class Base
{
    public void M(Span<string> s)
    {
        Console.WriteLine("Base");
    }

    public void M(int i)
    {
        Console.WriteLine("Base");
    }
}

class Derived : Base
{
    public static implicit operator Derived(ReadOnlySpan<string> r) => new Derived();
    public static implicit operator Derived(long l) => new Derived();

    public void M(Derived s)
    {
        Console.WriteLine("Derived");
    }
}

另请参阅:https://github.com/dotnet/roslyn/issues/78314

扩展方法查找

通过在扩展方法查找中允许进行隐式 Span 转换,我们可能会改变重载解析所解析的扩展方法。

namespace N1
{
    using N2;

    public class C
    {
        public static void M()
        {
            Span<string> span = new string[0];
            span.Test(); // Prints N2 today, N1 tomorrow
        }
    }

    public static class N1Ext
    {
        public static void Test(this ReadOnlySpan<string> span)
        {
            Console.WriteLine("N1");
        }
    }
}

namespace N2
{
    public static class N2Ext
    {
        public static void Test(this Span<string> span)
        {
            Console.WriteLine("N2");
        }
    }
}

开放性问题

不受限制的优先规则

我们应该无条件地对LangVersion制定 更好的规则 吗? 这将允许 API 作者添加新的 Span API,其中 IEnumerable 等效项存在,而不会破坏旧版 LangVersions 或其他编译器或语言(例如 VB)上的用户。 但是,这意味着用户在更新工具集后可能会有不同的行为(无需更改 LangVersion 或 TargetFramework):

  • 编译器可能会选择不同的重载(从技术角度看是破坏性的变更),但希望这些重载具有等效的行为。
  • 目前可能会出现其他中断,暂不可知。

请注意, OverloadResolutionPriorityAttribute 无法完全解决此问题,因为它也会在较旧的 LangVersions 上被忽略。 应该可以使用它来避免 VB 中关于属性识别的歧义。

忽略更多用户定义的转换

我们定义了一组类型对,其中存在语言定义的隐式和显式跨度转换。 每当存在T1T2的语言定义范围转换时,任何从T1T2的用户定义转换都会被忽略(无论范围和用户定义的转换是隐式的还是显式的)。

请注意,这包括所有条件。例如,没有从Span<object>ReadOnlySpan<string>的跨度转换(而是从Span<T>ReadOnlySpan<U>的转换,但必须满足T : U这个条件),因此,即使存在用户定义的转换,也只能在这些类型之间进行考虑(这必须是专门的转换,如Span<T>ReadOnlySpan<string>,因为转换运算符不能有泛型参数)。

是否还应忽略数组/Span/ReadOnlySpan/字符串类型的其他组合之间的用户定义的转换,其中不存在相应的语言定义跨度转换? 例如,如果有从ReadOnlySpan<T>Span<T>的用户定义转换,我们应该忽略它吗?

应考虑以下规范可能情况:

  1. 每当存在从T1T2的跨度转换时,请忽略任何用户定义的从T1T2的转换或从T2T1的转换。

  2. 在两者之间转换时,不考虑用户定义的转换

    • 任何单维 array_typeSystem.Span<T>/System.ReadOnlySpan<T>
    • System.Span<T> / System.ReadOnlySpan<T> 的任意组合,
    • stringSystem.ReadOnlySpan<char>
  3. 如上所示,但将最后一个项目符号替换为:
    • stringSystem.Span<char>/System.ReadOnlySpan<char>
  4. 如上所示,但将最后一个项目符号替换为:
    • stringSystem.Span<T>/System.ReadOnlySpan<T>

从技术上看,规范禁止其中一些用户定义转换甚至被定义:不可能在存在非用户定义的转换的类型(§10.5.2)之间定义用户定义的运算符。 不过,Roslyn 有意违反了规范的这一部分。尽管如此,某些转换(例如,允许在 Spanstring 之间的转换)仍被允许(这些类型之间不存在任何语言定义的转换)。

但是,除了只忽略这些转换外,我们还可彻底禁止它们被定义,并可能会至少对这些新的 Span 转换打破规范冲突;例如,如果定义了这些转换(可能不含 BCL 已定义的转换),则更改 Roslyn 以实际报告编译时错误。

替代方案

保持现状。