備註
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異已記錄在相關的 語言設計會議(LDM)備忘錄中。
您可以在 規範的文章中深入瞭解將功能規範納入 C# 語言標準的過程。
Champion 期數: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。
範圍轉換
我們會將一種新的隱含轉換類型新增至 §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),則這些類型皆適用於轉換。
我們也會將 隱含範圍轉換 新增至標準隱含轉換清單 ()。 這可讓多載解析在執行參數解析時將其納入考量,如同先前連結的 API 提案一樣。
明確的範圍轉換如下:
- 所有 隱含範圍轉換。
- 從具有項目類型 的
Ti到System.Span<Ui>或System.ReadOnlySpan<Ui>,前提是從Ti到Ui存在明確的參考轉換。
不同於其他 標準明確轉換(§10.4.3),並不存在標準明確跨度轉換;這通常是因為存在相反的標準隱含轉換。
使用者定義的轉換
在隱含或明確範圍轉換的類型之間轉換時,不會考慮使用者定義的轉換。
隱含跨度轉換不受規則限制,即在非使用者自定義轉換存在的類型之間定義使用者自定義運算子是不可能的(§10.5.2 允許的使用者自定義轉換)。 這是必要的,因此 BCL 可以繼續定義現有的 Span 轉換運算子,即使它們切換至 C# 14(它們對於較低版本的 LangVersions 仍然需要,也因為這些運算子用於新的標準 Span 轉換的程式碼生成)。 但它可以視為實作詳細數據(codegen 和較低的 LangVersions 不是規格的一部分),Roslyn 無論如何都會違反規格的這個部分(不會強制執行關於使用者定義轉換的這個特定規則)。
擴充接收器
我們也在判斷適用性時,將 隱含範圍轉換 新增至擴充方法第一個參數上可接受的隱含轉換清單(12.8.9.3) (粗體變更):
如果符合資格,
Cᵢ.Mₑ擴充方法 符合資格:
Cᵢ是非泛型非巢狀類別Mₑ的名稱是 識別碼Mₑ可存取且適用於套用至自變數作為靜態方法,如上所示- 隱含身分識別、參考
或 boxing、boxing 或 span 轉換存在,從 expr 到Mₑ的第一個參數類型。 在為方法群組轉換執行過載解析時,Span 轉換不會被考慮。
請注意,在方法群組轉換中,擴充接收器不會考慮隱式範圍轉換(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 多載。
- 編譯程式可能會生成一個 thunk,將陣列當作接收者,並在內部執行 span 轉換(類似於使用者手動建立委派,例如
x => new int[0].M(x))。 - 如果實現值委派,它可以直接將
Span當作接收者。
方差
隱含範圍轉換 中的變異數段目標是模仿 System.ReadOnlySpan<T>的一些共變異數。 您必須進行運行時間變更,才能透過泛型完整實作變數(請參閱 。。/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是否為非 null,並且是否可以透過參考轉換、Boxing 轉換、Unboxing 轉換、包裝轉換或解除包裝轉換成功轉換為類型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 運算符則不存在)。
顯式陣列到 ReadOnlySpan 轉換首先將來源陣列顯式轉換為具有目標元素類型的陣列,然後使用與隱式轉換相同的輔助方法轉換為 ReadOnlySpan,也就是對應的 op_Implicit(T[])。
從表達式進行更好的轉換
從表達式進行的較佳轉換 (§12.6.4.5)已更新為更偏好隱式的範圍轉換。 這是以 集合表示式多載解析變更為基礎,。
假設隱含轉換
C₁會從表達式E轉換成類型T₁,以及從表達式C₂轉換成類型E的隱含轉換T₂,如果下列其中一項保留,C₁:
E是 集合表達式,而C₁是一種比 更好的表達式的集合轉換C₂E不是 集合表示式,且符合以下其中一項條件:
E完全符合T₁,而E與T₂不完全相符E都不完全符合T₁和T₂,而C₁是隱含範圍轉換,C₂則不是隱含範圍轉換E完全符合T₁和T₂中的任一或皆非,C₁和C₂兩者均為隱含範圍轉換,T₁是比更好的轉換目標。E是方法群組,T₁與方法群組中轉換C₁的單一最佳方法相容,而T₂與方法群組中用於轉換的單一最佳方法不相容C₂
更好的轉換目標
更好的轉換目標(§12.6.4.7)更新為更偏好 ReadOnlySpan<T> 而不是 Span<T>。
假設有兩種類型
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₁或T₂不是System.ReadOnlySpan<Eᵢ>,並且不是System.Span<Eᵢ>,並且 從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
優化備註
較佳的轉換來自表示式 規則應確保每當多載因新的範圍轉換而適用時,就會避免與另一個多載的任何潛在模棱兩可,因為會比較偏好新適用的多載。
如果沒有此規則,下列在 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的範圍轉換而定義的,因此,如果 API 作者想要在 LangVersion <= 13上繼續支援使用者,就無法新增此類新重載。
例如,如果 .NET 9 的 BCL 引進這類多載,那麼升級至 net9.0 TFM 的使用者但保持較低的 LangVersion,將會對現有程式碼收到模糊不清的錯誤訊息。
另請參閱以下 開放的問題。
類型推斷
我們會更新規格的類型推斷區段,如下所示(粗體中的變更)。
12.6.3.9 確切推斷
從 推斷,如下所示:
- 如果
V是其中一個 未固定的Xᵢ,則會將U新增至Xᵢ的確切界限集。- 否則,會藉由檢查下列任何情況是否適用來決定
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是 未固定的Xᵢ之一,那麼U會被新增到Xᵢ的下限集合中。- 否則,如果
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(這些不能是參考型別)。
不過,只有在來源類型是 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> 的多載則不行,因為跨度轉換會對協變陣列擲回 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。
表達式樹
採用 MemoryExtensions.Contains 等跨度的重載比傳統重載優先,例如 Enumerable.Contains,即使是在表达式树內,但解釋器引擎不支援 ref struct。
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
擴充方法查找
藉由在擴充方法查找中允許 隱含範圍轉換,我們可能會影響多載解析所選擇的擴充方法。
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 作者在存在 IEnumerable 等效項的情況下,新增新的 Span API,而不會影響使用舊版語言版本或其他編譯器或語言(例如 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)。
但羅斯林故意違反這一部分的規範。儘管如此,仍允許 Span 與 string 之間的某些轉換(因為這些類型之間沒有語言定義的轉換)。
不過,與其僅僅忽略這些轉換,我們可以考慮完全不允許它們被定義,並試圖在至少這些新的範圍轉換上修正規格違規,也就是說,修改 Roslyn 以在這些轉換被定義時實際報告編譯時錯誤(可能不包括那些 BCL 已經定義的轉換)。
替代選擇
保持事物的現狀。