共用方式為


範圍

注意

本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。

功能規格與已完成實作之間可能有一些差異。 這些差異是在相關的 語言設計會議(LDM)紀錄中擷取的。

您可以在規範的文章中深入瞭解將功能規範納入C# 語言標準的過程

總結

這項功能是傳遞兩個新的運算符,以允許建構 System.IndexSystem.Range 物件,並在運行時間使用這些運算符來編製/配量集合。

概述

已知的類型和成員

若要針對 System.IndexSystem.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。 如果 LengthCount 都存在,建議使用 Length。 為了簡單起見,提案會使用名稱 Length 來代表 CountLength

針對這類類型,語言會模擬出一個類似 T this[Index index] 的索引器成員,其中 Tint 為基礎的索引器的回傳型別,包括任何 ref 樣式的註解。 新的成員將會擁有與int索引器相同的getset成員,且它們的存取權限一致。

新的索引器會藉由將類型 Index 的自變數轉換成 int,併發出 int 型索引器呼叫來實作。 為了討論目的,讓我們使用 receiver[expr]的範例。 將 expr 轉換成 int,如下所示:

  • 當自變數的格式為 ^expr2,且 expr2 的類型為 int時,它會轉譯為 receiver.Length - expr2
  • 否則,它會轉譯為 expr.GetOffset(receiver.Length)

不論特定轉換策略為何,評估的順序都應該等於下列各項:

  1. receiver 已經被評估
  2. expr 被評估
  3. 在需要時對 length 進行評估;
  4. int 為基礎的索引器被調用。

這可讓開發人員在現有類型上使用 Index 功能,而不需要修改。 例如:

List<char> list = ...;
var value = list[^1];

// Gets translated to
var value = list[list.Count - 1];

receiverLength 表示式會適當地溢出,以確保任何副作用只會執行一次。 例如:

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] 的索引器成員,其中 TSlice 方法的傳回類型,包括任何 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 自變數時重複使用。 當這樣做時,將其稱為 startSlice 的第二個參數將透過以下方式轉換範圍類型表達式來獲得:

  • 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

不論特定轉換策略為何,評估的順序都應該等於下列各項:

  1. receiver 已評估
  2. expr 被評估中;
  3. length 視需要進行評估;
  4. 將會調用 Slice 方法。

receiverexprlength 表示式將被適當處理,以確保任何副作用僅執行一次。 例如:

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.IndexSystem.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 和陣列。

僅偵測計數

在屬性名稱上偵測 CountLength 確實會使設計複雜一點。 只挑選一個來標準化 ,但還不夠,因為它最終排除了大量的類型:

  • 使用 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)

receiverLength 表示式會適當地溢出,以確保任何副作用只會執行一次。 例如:

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 參數
  • 尋找模式成員時,我們會尋找原始定義,而不是建構的成員

設計會議