範圍
注意
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異是在相關的
總結
這項功能是傳遞兩個新的運算符,以允許建構 System.Index
和 System.Range
物件,並在運行時間使用這些運算符來編製/配量集合。
概述
已知的類型和成員
若要針對 System.Index
和 System.Range
使用新的語法形式,可能需要新的已知類型和成員,視使用語法形式而定。
若要使用「hat」運算子(^
),需要下列條件
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
若要在陣列元素存取中使用 System.Index
型別做為自變數,需要下列成員:
int System.Index.GetOffset(int length);
..
語法對於 System.Range
需要 System.Range
類型,並且至少需要下列一個或多個成員:
namespace System
{
public readonly struct Range
{
public Range(System.Index start, System.Index end);
public static Range StartAt(System.Index start);
public static Range EndAt(System.Index end);
public static Range All { get; }
}
}
..
語法允許其自變數都不存在或沒有任何自變數。 不論自變數數目為何,Range
建構函式一律足以使用 Range
語法。 然而,若有其他的成員出現且缺少一或多個 ..
的自變數,則可以替換成適當的成員。
最後,若要在數位元素存取表達式中使用類型 System.Range
的值,必須存在下列成員:
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}
System.Index
C# 無法從結尾編製集合的索引,但大部分的索引器都會使用 “from start” 概念,或執行 “length - i” 表達式。 我們引入了一個新的 Index 表達式,表示「從末端開始」。 此功能將引進新的一元前置詞 「hat」 運算子。 其單一操作數必須可轉換成 System.Int32
。 它會被嵌入到適當的 System.Index
工廠方法呼叫中。
我們會使用下列其他語法形式來增強 unary_expression 文法:
unary_expression
: '^' unary_expression
;
我們會從結尾 運算符呼叫此
System.Index operator ^(int fromEnd);
這個運算子的行為只會針對大於或等於零的輸入值定義。
例子:
var array = new int[] { 1, 2, 3, 4, 5 };
var thirdItem = array[2]; // array[2]
var lastItem = array[^1]; // array[new Index(1, fromEnd: true)]
System.Range
C# 沒有語法來存取集合的「範圍」或「切片」。 通常使用者被迫實作複雜的結構,以篩選或操作記憶體片段,或訴諸 LINQ 方法,例如 list.Skip(5).Take(2)
。 隨著新增 System.Span<T>
和其他類似類型,在語言/執行階段的更深層次上支援這類操作,並統一介面變得更為重要。
語言會在 x..y
引進新的範圍運算元。 它是接受兩個表達式的二進位中置運算符。 您可以省略任一操作數(以下範例),而且它們必須可轉換成 System.Index
。 它會降低到適當的 System.Range
Factory 方法呼叫。
我們將 multiplicative_expression 的 C# 文法規則取代為下列內容,以便引入新的優先權層級:
range_expression
: unary_expression
| range_expression? '..' range_expression?
;
multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;
所有形式的 範圍運算子 具有相同的優先順序。 這個新的優先順序群組低於
我們將 ..
運算子稱為 範圍運算子。 內建範圍運算符可以大致理解為對應於此形式內建運算符的調用。
System.Range operator ..(Index start = 0, Index end = ^0);
例子:
var array = new int[] { 1, 2, 3, 4, 5 };
var slice1 = array[2..^3]; // array[new Range(2, new Index(3, fromEnd: true))]
var slice2 = array[..^3]; // array[Range.EndAt(new Index(3, fromEnd: true))]
var slice3 = array[2..]; // array[Range.StartAt(2)]
var slice4 = array[..]; // array[Range.All]
此外,System.Index
應該有來自 System.Int32
的隱含轉換,以避免需要在多維度簽章上多載混合整數和索引。
將索引和範圍支援新增至現有的程式庫類型
隱含索引支援
語言會針對符合下列準則的類型,提供實例索引器成員,並提供類型為 Index
的單一參數:
- 此類型為 Countable。
- 此類型具有可存取的實例索引器,它會採用單一
int
做為自變數。 - 此類型沒有可存取的實例索引器,它會接受
Index
做為第一個參數。Index
必須是唯一的參數,或其餘參數必須是選擇性參數。
若某類型具有名為 Length
的屬性或具有具可存取 getter 的 Count
並且傳回型別為 int
的屬性,則該類型為 Countable。 語言可以利用此屬性將類型為 Index
的運算式在表達式處轉換為 int
,而完全不需要使用類型 Index
。 如果 Length
和 Count
都存在,建議使用 Length
。 為了簡單起見,提案會使用名稱 Length
來代表 Count
或 Length
。
針對這類類型,語言會模擬出一個類似 T this[Index index]
的索引器成員,其中 T
是 int
為基礎的索引器的回傳型別,包括任何 ref
樣式的註解。 新的成員將會擁有與int
索引器相同的get
和set
成員,且它們的存取權限一致。
新的索引器會藉由將類型 Index
的自變數轉換成 int
,併發出 int
型索引器呼叫來實作。 為了討論目的,讓我們使用 receiver[expr]
的範例。 將 expr
轉換成 int
,如下所示:
- 當自變數的格式為
^expr2
,且expr2
的類型為int
時,它會轉譯為receiver.Length - expr2
。 - 否則,它會轉譯為
expr.GetOffset(receiver.Length)
。
不論特定轉換策略為何,評估的順序都應該等於下列各項:
-
receiver
已經被評估 -
expr
被評估 - 在需要時對
length
進行評估; - 以
int
為基礎的索引器被調用。
這可讓開發人員在現有類型上使用 Index
功能,而不需要修改。 例如:
List<char> list = ...;
var value = list[^1];
// Gets translated to
var value = list[list.Count - 1];
receiver
和 Length
表示式會適當地溢出,以確保任何副作用只會執行一次。 例如:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int this[int index] => _array[index];
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get()[^1];
Console.WriteLine(i);
}
}
此程式代碼會列印「取得長度 3」。
隱式範圍支援
語言會針對符合下列準則的類型,提供實例索引器成員,並提供類型為 Range
的單一參數:
- 此類型為 Countable。
- 此類型具有一個名為
Slice
的可存取成員,該成員有兩個類型為int
的參數。 - 此類型沒有實例索引器,它會採用單一
Range
做為第一個參數。Range
必須是唯一的參數,或其餘參數必須是選擇性參數。
針對這類類型,語言會綁定成為類似於格式 T this[Range range]
的索引器成員,其中 T
是 Slice
方法的傳回類型,包括任何 ref
風格註解。 新成員將擁有與 Slice
相匹配的無障礙設計。
當 Range
型索引器系結在名為 receiver
的運算式上時,會將 Range
表達式轉換成兩個值,然後傳遞至 Slice
方法,以降低此索引器。 為了討論目的,讓我們使用 receiver[expr]
的範例。
Slice
的第一個自變數會透過下列方式轉換範圍型別表示式來取得:
- 當
expr
的格式為expr1..expr2
(其中可以省略expr2
),且expr1
的類型為int
,則會發出為expr1
。 - 當
expr
格式為^expr1..expr2
(其中可以省略expr2
),則會發出為receiver.Length - expr1
。 - 當
expr
格式為..expr2
(其中可以省略expr2
),則會發出為0
。 - 否則,它將以
expr.Start.GetOffset(receiver.Length)
發出。
此值將會在計算第二個 Slice
自變數時重複使用。 當這樣做時,將其稱為 start
。
Slice
的第二個參數將透過以下方式轉換範圍類型表達式來獲得:
- 當
expr
的格式為expr1..expr2
(其中可以省略expr1
),且expr2
的類型為int
,則會發出為expr2 - start
。 - 當
expr
格式為expr1..^expr2
(其中可以省略expr1
),則會發出為(receiver.Length - expr2) - start
。 - 當
expr
格式為expr1..
(其中可以省略expr1
),則會發出為receiver.Length - start
。 - 否則,它將被發射為
expr.End.GetOffset(receiver.Length) - start
。
不論特定轉換策略為何,評估的順序都應該等於下列各項:
-
receiver
已評估 -
expr
被評估中; -
length
視需要進行評估; - 將會調用
Slice
方法。
receiver
、expr
和 length
表示式將被適當處理,以確保任何副作用僅執行一次。 例如:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int[] Slice(int start, int length) {
var slice = new int[length];
Array.Copy(_array, start, slice, 0, length);
return slice;
}
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
var array = Get()[0..2];
Console.WriteLine(array.Length);
}
}
此程式代碼會列印「取得長度 2」。
語言將針對以下已知類型進行特別處理:
-
string
:將會使用 方法Substring
,而不是使用Slice
。 -
array
:將會使用 方法System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
,而不是使用Slice
。
替代方案
新的運算子(^
和 ..
)是語法糖。 此功能可以透過明確呼叫 System.Index
和 System.Range
Factory 方法來實作,但會導致更多的未定案程序代碼,而且體驗會不合預期。
IL 表示法
這兩個運算符會降低為一般索引器/方法呼叫,後續編譯程式層不會有任何變更。
運行時間行為
- 編譯器可以針對內建類型(如陣列和字串)的索引器進行優化,並將索引降至適當的現有方法。
- 如果建構為負值,
System.Index
將會擲回。 -
^0
不會擲回,但它會轉譯為提供給它的集合/可列舉的長度。 -
Range.All
語意上相當於0..^0
,而且可以解構為這些索引。
考慮
根據 ICollection 偵測可編索引元素
此行為的靈感來自於集合初始化器。 使用型別的結構來傳達它已選擇加入功能。 在集合初始化類型的情況下,可以藉由實作介面 IEnumerable
(非泛型)來加入功能。
此提案最初要求類型必須實作 ICollection
,才能符合索引化的資格。 不過,這需要一些特殊案例:
-
ref struct
:這些無法實作介面,但Span<T>
這類類型非常適合索引/範圍支援。 -
string
:不會實作ICollection
並新增該介面的成本很大。
這表示已經需要支援金鑰類型的特殊大小寫。
string
的特殊處理不太重要,因為這個語言在其他領域也進行了類似的處理(foreach
轉小寫、常數處理等...)。ref struct
的特殊處理更令人關注,因為它特別針對整個類別的型別進行處理。 如果它們擁有一個名為 Count
的屬性,且傳回類型為 int
,它們就會標示為 Indexable。
經過考慮後,設計已標準化,表示任何具有屬性 Count
/ Length
且傳回類型為 int
的類型都是 Indexable。 這會移除所有特殊處理,即使是針對 string
和陣列。
僅偵測計數
在屬性名稱上偵測 Count
或 Length
確實會使設計複雜一點。 只挑選一個來標準化 ,但還不夠,因為它最終排除了大量的類型:
- 使用
Length
:排除幾乎所有 System.Collections 和子命名空間中的集合。 這些通常源自ICollection
,因此比起長度,更偏好Count
。 - 使用
Count
:排除string
、陣列、Span<T>
和大多數以ref struct
為基礎的類型
可索引型別初始偵測的額外複雜度,超過其在其他方面的簡化。
選擇「Slice」作為名稱
選擇名稱 Slice
,是因為它實際上是 .NET 中切片操作的標準名稱。 從 netcoreapp2.1 開始,所有 Span 類型在切片操作中都使用名稱 Slice
。 在 netcoreapp2.1 之前,實際上沒有任何可以作為範例參考的分片例子。
List<T>
、ArraySegment<T>
、SortedList<T>
等類型非常適合切割,但新增類型時,概念並不存在。
因此,Slice
作為唯一的範例,它被選為名稱。
索引目標類型轉換
在索引器表達式中將 Index
轉換視為目標類型轉換的另一種方式。 語言會將目標型別轉換指派給 int
,而不是將其綁定為類似於 return_type this[Index]
形式的成員。
這個概念可以一般化為可計數類型上的所有成員存取。 每當類型為 Index
的運算式被用作實例成員調用的引數,且接收者為 Countable 時,該運算式將進行目標型別轉換為 int
。 適用於此轉換的成員呼叫包括方法、索引器、屬性、擴充方法等等...只有建構函式會排除,因為它們沒有接收者。
對於類型為 Index
的任何運算式,將實作目標型別轉換。 為了討論,讓我們使用 receiver[expr]
範例。
- 當
expr
的格式為^expr2
,且expr2
的類型為int
時,將會轉譯為receiver.Length - expr2
。 - 否則,它會轉譯為
expr.GetOffset(receiver.Length)
。
receiver
和 Length
表示式會適當地溢出,以確保任何副作用只會執行一次。 例如:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int GetAt(int index) => _array[index];
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get().GetAt(^1);
Console.WriteLine(i);
}
}
此程式代碼會列印「取得長度 3」。
這項功能對於有參數代表索引的任何成員都有幫助。 例如,List<T>.InsertAt
。 這也有可能造成混淆,因為語言無法提供任何指引,說明表達式是否用於編製索引。 其所能做的就是在叫用 Countable 類型的成員時,將任何 Index
表達式轉換成 int
。
限制:
- 只有當類型為
Index
的表達式直接作為成員函數的參數時,才適用這項轉換。 它不適用於任何巢狀表達式。
在實作期間做出的決策
- 模式中的所有成員都必須是實例成員
- 如果找到 Length 方法,但其傳回類型不正確,請繼續尋找 Count 方法。
- 索引模式所用的索引器必須僅有一個 int 參數
- 用於 Range 模式的
Slice
方法必須正好有兩個 int 參數 - 尋找模式成員時,我們會尋找原始定義,而不是建構的成員