注释
本文是功能规格说明。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 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_types
System.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
- 从
string
到System.ReadOnlySpan<char>
如果任意 Span/ReadOnlySpan 类型均为 ref struct
,并且它们可通过完全限定的名称 (LDM 2024-06-24) 进行匹配,那么这些类型就被视为适用于该转换。
我们还将 隐式跨度转换添加到标准隐式转换 列表(§10.4.2)。 这允许重载解析在执行参数解析时考虑它们,如之前链接的 API 建议所示。
显式 Span 转换如下:
- 所有隐式 Span 转换。
- 从元素类型为 的
Ti
转换为System.Span<Ui>
或System.ReadOnlySpan<Ui>
,但前提是存在从Ti
到Ui
的显式引用转换。
与其他标准显式转换 (§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 的值类型,则如果true
且D
的类型相同,则结果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₂
:
更好的转换目标
更好的转换目标(§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₁
- ...
设计会议:
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-04.md#preferring-readonlyspant-over-spant-conversions
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-09.md#first-class-span-open-questions
优先注释
更好的表达式转换规则应确保每当由于新的 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₁[...]
V
是Span<V₁>
,U
是一个数组类型U₁[]
或Span<U₁>
V
是ReadOnlySpan<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₁[...]
V
是Span<V₁>
,U
是一个数组类型U₁[]
或Span<U₁>
V
是ReadOnlySpan<V₁>
,而U
是数组类型U₁[]
或Span<U₁>
或ReadOnlySpan<U₁>
V
是IEnumerable<V₁>
、ICollection<V₁>
、IReadOnlyList<V₁>>
、IReadOnlyCollection<V₁>
或IList<V₁>
之一,U
是单维数组类型U₁[]
V
是一个构造的class
、struct
、interface
或delegate
类型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₁
可能是X
或Y
。
如果这些情况中的任何一种适用,则从每个Uᵢ
到相应的Vᵢ
进行推理,如下所示:- 如果不知道
Uᵢ
是引用类型,则会进行确切推定- 否则,如果
U
是数组类型,则进行下限推理推理取决于V
的类型。
- 如果
V
是Span<Vᵢ>
,则进行确切推理- 如果
V
为数组类型或数组ReadOnlySpan<Vᵢ>
类型,则进行下限推理- 否则,如果
U
是Span<Uᵢ>
,则推断取决于V
的类型。
- 如果
V
是Span<Vᵢ>
,则进行确切推理- 如果
V
是ReadOnlySpan<Vᵢ>
,则进行下限推理- 否则,如果
U
是ReadOnlySpan<Uᵢ>
,V
是ReadOnlySpan<Vᵢ>
,则进行下限推理:- 否则,如果
V
是C<V₁...Vₑ>
,那么推理依赖于i-th
的C
类型参数:
- 如果是协变的,那么会进行 下限推理。
- 如果它是逆变类型的,则会进行上限推定。
- 如果它是固定的,则会进行确切推定。
- 否则,不进行推理。
没有上限推理规则,因为无法命中它们。
类型推理从不从上限开始,其过程必须经过下限推理和逆变类型参数。
由于规则“如果 Uᵢ
未知为引用类型,则进行 确切推理 ”,源类型参数不能 Span
/ReadOnlySpan
(不能为引用类型)。
但是,上限范围推理仅在源类型为 a Span
/ReadOnlySpan
时适用,因为它的规则如下:
U
是Span<U₁>
,V
是一个数组类型V₁[]
或Span<V₁>
U
是ReadOnlySpan<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://developercommunity.visualstudio.com/t/Extension-method-SystemLinqEnumerable/10790323
- https://developercommunity.visualstudio.com/t/Compilation-Error-When-Calling-Reverse/10818048
- https://developercommunity.visualstudio.com/t/Version-17131-has-an-obvious-defect-th/10858254
- https://developercommunity.visualstudio.com/t/Visual-Studio-2022-update-breaks-build-w/10856758
- https://github.com/dotnet/runtime/issues/111532
- https://developercommunity.visualstudio.com/t/Backward-compatibility-issue-:-IEnumerab/10896189#T-ND10896782
设计会议: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
,则需要对此作出反应。
另请参阅:
- https://github.com/dotnet/runtime/issues/109757
- https://github.com/dotnet/docs/issues/43952
- https://github.com/dotnet/efcore/issues/35100
- https://github.com/dotnet/csharplang/discussions/8959
设计会议:
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-04.md#conversions-in-expression-trees
- https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-01-06.md#ignoring-ref-structs-in-expressions
通过继承实现的用户定义的转换
通过将隐式跨度转换添加到 标准隐式转换 列表,我们可以在类型层次结构中涉及用户定义的转换时更改行为。 此示例显示了此更改,以便与已与新的 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 中关于属性识别的歧义。
忽略更多用户定义的转换
我们定义了一组类型对,其中存在语言定义的隐式和显式跨度转换。
每当存在T1
到T2
的语言定义范围转换时,任何从T1
到T2
的用户定义转换都会被忽略(无论范围和用户定义的转换是隐式的还是显式的)。
请注意,这包括所有条件。例如,没有从Span<object>
到ReadOnlySpan<string>
的跨度转换(而是从Span<T>
到ReadOnlySpan<U>
的转换,但必须满足T : U
这个条件),因此,即使存在用户定义的转换,也只能在这些类型之间进行考虑(这必须是专门的转换,如Span<T>
到ReadOnlySpan<string>
,因为转换运算符不能有泛型参数)。
是否还应忽略数组/Span/ReadOnlySpan/字符串类型的其他组合之间的用户定义的转换,其中不存在相应的语言定义跨度转换?
例如,如果有从ReadOnlySpan<T>
到Span<T>
的用户定义转换,我们应该忽略它吗?
应考虑以下规范可能情况:
-
每当存在从
T1
到T2
的跨度转换时,请忽略任何用户定义的从T1
到T2
的转换或从T2
到T1
的转换。 -
在两者之间转换时,不考虑用户定义的转换
- 任何单维
array_type
和System.Span<T>
/System.ReadOnlySpan<T>
, -
System.Span<T>
/System.ReadOnlySpan<T>
的任意组合, -
string
和System.ReadOnlySpan<char>
。
- 任何单维
- 如上所示,但将最后一个项目符号替换为:
-
string
和System.Span<char>
/System.ReadOnlySpan<char>
。
-
- 如上所示,但将最后一个项目符号替换为:
-
string
和System.Span<T>
/System.ReadOnlySpan<T>
。
-
从技术上看,规范禁止其中一些用户定义转换甚至被定义:不可能在存在非用户定义的转换的类型(§10.5.2)之间定义用户定义的运算符。
不过,Roslyn 有意违反了规范的这一部分。尽管如此,某些转换(例如,允许在 Span
和 string
之间的转换)仍被允许(这些类型之间不存在任何语言定义的转换)。
但是,除了只忽略这些转换外,我们还可彻底禁止它们被定义,并可能会至少对这些新的 Span 转换打破规范冲突;例如,如果定义了这些转换(可能不含 BCL 已定义的转换),则更改 Roslyn 以实际报告编译时错误。
替代方案
保持现状。