共用方式為


集合表達式

注意

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

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

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

總結

集合表達式引入了一種新的簡潔語法[e1, e2, e3, etc],用於創建常見的集合值。 將其他集合內嵌到這些值中,可能會使用散佈元素 ..e,如下所示:[e1, ..c2, e2, ..c2]

您可以建立數個類似集合的類型,而不需要外部 BCL 支援。 這些類型包括:

針對上述未涵蓋的集合型別提供進一步支援,其方式是透過直接在型別本身上採用的新屬性和API模式。

動機

  • 類似集合的值在程序設計、演算法中非常存在,特別是在 C#/.NET 生態系統中。 幾乎所有程式都會使用這些值來儲存數據,並從其他元件傳送或接收數據。 目前,幾乎所有 C# 程式都必須使用許多不同且不幸地冗長的方式來創建這類值的實例。 有些方法也有效能缺點。 以下是一些常見的範例:

    • 陣列,在 { ... } 值之前需要 new Type[]new[]
    • Spans,其可能會使用 stackalloc 和其他複雜的結構。
    • 集合初始設定器需使用類似 new List<T> 的語法(缺乏對可能過於冗長的 T的推論),而這可能會導致多次記憶體重新配置,因為它們使用 N 次 .Add 呼叫而未提供初始容量。
    • 不可變的集合,其需要語法,例如 ImmutableArray.Create(...) 來初始化值,這可能會導致中繼配置和數據複製。 更有效率的建築形式(如 ImmutableArray.CreateBuilder)是笨拙的,仍然產生不可避免的垃圾。
  • 看看周圍的生態系統,我們也發現到處都有範例,這些範例使清單建立變得更方便和愉悅的使用體驗。 TypeScript、Dart、Swift、Elm、Python 和更多選擇針對此目的使用簡潔的語法,並廣泛使用,而且效果非常出色。 初步調查顯示,這些生態系統中因內建這些字面值而未產生任何實質性問題。

  • C# 也已在 C# 11 中新增 清單模式。 此模式允許使用全新且直覺式的語法來比對和解構類似清單的值。 不過,與幾乎所有其他模式建構不同,此比對/解構語法缺少對應的建構語法。

  • 取得建構每個集合類型的最佳效能可能很棘手。 簡單的解決方案通常會浪費 CPU 和記憶體。 具備字面值形式可讓編譯器實作具有最大的彈性,以優化字面值,產生至少與使用者所能提供的結果一樣好,但使用簡單的程式碼。 編譯程式通常能夠做得更好,而規格的目標是讓實作在實作策略方面有大量的迴旋餘地,以確保這一點。

C# 需要包容性解決方案。 它應該符合客戶在已擁有的集合類型與值方面的絕大多數情況。 它也應該在語言中顯得自然,並且反映在模式比對中所做的工作。

由此得出一個自然結論,即語法應像 [e1, e2, e3, e-etc][e1, ..c2, e2],這些與 [p1, p2, p3, p-etc][p1, ..p2, p3]的模式等價項相對應。

詳細設計

新增下列 文法 生成規則:

primary_no_array_creation_expression
  ...
+ | collection_expression
  ;

+ collection_expression
  : '[' ']'
  | '[' collection_element ( ',' collection_element )* ']'
  ;

+ collection_element
  : expression_element
  | spread_element
  ;

+ expression_element
  : expression
  ;

+ spread_element
  : '..' expression
  ;

集合常值 目標型別

規格說明

  • 為了簡潔起見,collection_expression 將在下列各節中稱為「常值」。

  • expression_element 實例通常稱為 e1e_n等。

  • spread_element 實例通常稱為 ..s1..s_n等。

  • 範圍類型 表示 Span<T>ReadOnlySpan<T>

  • 常值通常會顯示為 [e1, ..s1, e2, ..s2, etc],以任何順序表示任意數目的元素。 重要的是,此表單將用來代表所有情況,例如:

    • 空常值 []
    • 不包含 expression_element 的文字常數。
    • 字面值中不包含 spread_element
    • 具有任意排序任何項目類型的常值。
  • ..s_n迭代類型迭代變數的類型, 確定為若在 foreach_statement中使用 s_n 作為進行迭代的表達式。

  • __name 開頭的變數用於表示對 name的評估結果,這些結果儲存在某個位置,以確保只進行一次評估。 例如,__e1 是評估 e1

  • List<T>IEnumerable<T>等,請參閱 System.Collections.Generic 命名空間中的個別類型。

  • 規格定義了將常值的 轉譯為現有 C# 建構的。 類似於 查詢表達式轉譯,字面量本身只有在翻譯會導致合法程式碼時才合法。 此規則的目的是避免重複隱含語言的其他規則(例如,指派至儲存位置時表達式的可轉換性)。

  • 實作不需要完全如以下指定來轉譯常值。 如果產生相同的結果,且結果生產中沒有任何可觀察的差異,則任何轉譯都是合法的。

    • 例如,實作可以將像是 [1, 2, 3] 之類的常值直接轉換成一個 new int[] { 1, 2, 3 } 表示式,該表示式本身將原始數據嵌入到組件中,從而簡化掉 __index 或一連串指令來指派每個值的需求。 重要的是,如果翻譯的任何步驟可能會導致運行時間發生例外狀況,表示程序狀態仍留在轉譯所指示的狀態。
  • 報告中提到的「堆疊配置」指的是任何在堆疊上進行配置的策略,而不是在堆上配置。 重要的是,它並不表示或要求該策略是透過實際的 stackalloc 機制。 例如,使用 內嵌陣列 也是在可用時實現堆疊配置的允許且理想的方法。 請注意,在 C# 12 中,無法使用集合表示式初始化內嵌陣列。 這仍然是一個公開的建議。

  • 假定集合有良好的行為。 例如:

    • 假設集合上的 Count 值會產生與列舉時元素數目相同的值。
    • System.Collections.Generic 命名空間中定義之此規格中使用的類型,假設為無副作用。 因此,編譯程式可以將這類類型可能用作中繼值的案例優化,否則不會公開。
    • 假設對集合某些適用的 .AddRange(x) 成員進行呼叫,將會產生與逐個遍歷 x 並將其所有枚舉值逐一新增至具有 .Add的集合相同的最終值。
    • 集合文字與非良性集合的行為是未定義的。

轉換

集合表示式轉換 可讓集合表達式轉換成類型。

從集合運算式隱含轉換成下列類型:

  • 單一維度的 陣列類型T[],在此情況下,元素類型T
  • 範圍類型
    • System.Span<T>
    • System.ReadOnlySpan<T>
      在此情況下,項目類型T
  • 具有適當 create 方法類型,在此情況下,專案類型反覆運算類型, 實例方法或可列舉的介面決定,而不是從擴充方法決定的 迭代類型
  • 實作 System.Collections.IEnumerable結構 類別類型:
    • 類型 有一個 適用的 建構函式,可以使用無自變數來叫用,而且建構函式可在集合表達式的位置存取。

    • 如果集合表達式有任何元素,則 類型 具有一個實例或擴充方法 Add,其中:

      • 方法可以使用單一值自變數來叫用。
      • 如果方法為泛型,可以從集合和自變數推斷類型自變數。
      • 方法可在集合表達式的位置存取。

      在此情況下,項目類型類型反覆運算類型

  • 介面類型:
    • System.Collections.Generic.IEnumerable<T>
    • System.Collections.Generic.IReadOnlyCollection<T>
    • System.Collections.Generic.IReadOnlyList<T>
    • System.Collections.Generic.ICollection<T>
    • System.Collections.Generic.IList<T>
      在此情況下,項目類型T

如果類型具有 項目類型U,並且集合表示式中每個 項目Eᵢ,則隱含轉換存在。

  • 如果 Eᵢ表示式項目,則會從 Eᵢ 隱含轉換成 U
  • 如果 散佈項目,則會從 反覆運算類型隱含轉換成

沒有將集合表達式 轉換成多維度 陣列類型集合運算式轉換。

從集合表達式轉換隱含集合表達式的類型是該集合表達式的有效 目標類型

集合表示式存在以下其他額外的隱含轉換:

  • 可為 Null 的實值型別,其中集合表達式 從集合運算式轉換成實值型別 。 轉換是 集合表示式轉換T,後面接著 TT?的隱含可為 Null 的轉換。

  • 對參考型別 T,其中 建立方法 與傳回類型 U隱含參考轉換UT相關聯的 T。 轉換是 集合表示式轉換U 後面接著從 UT隱含參考 轉換。

  • 對介面類型 I,其中有一個與 I 相關聯的 建立方法,該方法返回類型 V,並且從 VI隱含 boxing 轉換。 轉換是 集合表達式轉換V,後面接著從 VI隱含封箱 轉換。

建立方法

create 方法 會以 集合類型上的 [CollectionBuilder(...)] 屬性表示。 屬性會指定要叫用方法的 產生器類型方法名稱,以建構集合類型的實例。

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(
        AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface,
        Inherited = false,
        AllowMultiple = false)]
    public sealed class CollectionBuilderAttribute : System.Attribute
    {
        public CollectionBuilderAttribute(Type builderType, string methodName);
        public Type BuilderType { get; }
        public string MethodName { get; }
    }
}

屬性可以套用至 classstructref structinterface。 雖然屬性可以套用至基底 classabstract class,但不會繼承屬性。

產生器類型 必須是非泛型 classstruct

首先,確定套用的 建立方法CM 的集合。
它包含符合下列需求的方法:

  • 方法必須具有在 [CollectionBuilder(...)] 屬性中指定的名稱。
  • 方法必須直接在 建構器類型 上定義。
  • 方法必須 static
  • 使用集合表達式時,必須能夠存取 方法。
  • 方法的 元數 必須符合集合類型的 元數
  • 方法必須具有型別為 System.ReadOnlySpan<E>的單一參數,並由值傳遞。
  • 同一性轉換隱含參考轉換,或 裝箱轉換,從方法的傳回型別轉換為 集合類型

基底類型或介面上宣告的方法會被忽略,而不是 CM 集的一部分。

如果 CM 集是空的,則 集合類型 沒有 項目類型 ,而且沒有 創建方法。 下列步驟都不適用。

如果 集合中只有一個方法具有 識別轉換, 轉換成 集合類型 的 項目類型也就是 集合類型的建立方法。 否則,集合類型 沒有 create 方法

如果 [CollectionBuilder] 屬性未參考具有預期簽章的可叫用方法,就會報告錯誤。

對於目標型別 C<S0, S1, …>集合表達式,其中的 型別宣告C<T0, T1, …> 具有相關聯的 產生器方法B.M<U0, U1, …>(),目標型別的 泛型型別自變數 會按順序套用,從最外層的包含型別到最內層,應用於 產生器方法

create 方法 的 span 參數可以明確標示為 scoped[UnscopedRef]。 如果參數是隱含或明確 scoped,編譯程式 可能會 配置堆疊上範圍的記憶體,而不是堆積。

例如,可能 ImmutableArray<T>建立方法

[CollectionBuilder(typeof(ImmutableArray), "Create")]
public struct ImmutableArray<T> { ... }

public static class ImmutableArray
{
    public static ImmutableArray<T> Create<T>(ReadOnlySpan<T> items) { ... }
}

使用上述的 create 方法ImmutableArray<int> ia = [1, 2, 3]; 可能會被發出為:

[InlineArray(3)] struct __InlineArray3<T> { private T _element0; }

Span<int> __tmp = new __InlineArray3<int>();
__tmp[0] = 1;
__tmp[1] = 2;
__tmp[2] = 3;
ImmutableArray<int> ia =
    ImmutableArray.Create((ReadOnlySpan<int>)__tmp);

建設

集合表達式的元素 依序評估,由左至右。 每個元素都會只評估一次,而元素的任何進一步參考都會參考此初始評估的結果。

在集合表達式中,可能會在後續元素被 評估之前或之後 遍歷

從建構期間使用的任何方法拋出的未處理例外狀況將不會被捕捉,並會阻止進一步的建構步驟。

LengthCountGetEnumerator 假設沒有任何副作用。


如果目標類型是 結構體類別類型,並且實作了 System.Collections.IEnumerable,但目標類型沒有 create 方法,那麼集合實例的建構如下:

  • 元素會依順序進行評估。 在下列步驟期間,某些或所有元素可能會被評估 ,而非提早於

  • 編譯程式 可以叫用 可計算的 屬性,或來自已知介面或類型的對等屬性,在每個 散布項目表達式上叫用集合表達式的 已知長度

  • 不帶參數的建構函式被叫用了。

  • 針對每個元素按照順序:

    • 如果元素是 表示式元素,則會以元素 表示式 作為參數來呼叫適用的 Add 實例或擴充方法。 (不同於傳統 集合初始化行為,元素評估和 Add 呼叫不一定交錯進行。)
    • 如果元素是 擴展元素,則會使用下列其中一項:
      • 散佈項目表達式 上叫用適用的 GetEnumerator 實例或擴充方法,然後針對從列舉值取得的每個專案,在 集合實例 上以該專案做為自變數,叫用適用的 Add 實例或擴充方法。 如果枚舉器實作了 IDisposable,那麼無論是否發生例外,都會在枚舉後呼叫 Dispose
      • 集合實例上會叫用適用的 AddRange 實例或擴充方法, 將散佈專案 表達式 做為自變數。
      • 使用集合實例和 索引做為自變數的 散佈項目表示式上叫用適用的 實例或擴充方法。
  • 在上述建構步驟期間, 實例或擴充方法 可能會 集合實例上叫用一或多次, 具有 容量自變數。


如果目標類型是 陣列範圍、具有 create 方法的類型,或 介面,則集合實例的建構如下:

  • 元素會依序進行評估。 某些或所有元素可能會在下列步驟 期間評估 ,而非在此之前。

  • 編譯器 可以透過在每個 散布項目表達式上叫用 可計算的 屬性或來自已知介面或類型的對等屬性,來決定集合表達式的 已知長度

  • 創建一個 初始化實例,如下所示:

    • 如果目標類型是 陣列,且集合表達式具有 已知長度,則會配置具有預期長度的陣列。
    • 如果目標類型是 範圍 或具有 create 方法的類型,且集合具有 已知長度,則會建立一個具有預期長度的跨度,這個跨度會參考連續記憶體儲存。
    • 否則會配置中繼記憶體。
  • 針對每個依順序的元素:

    • 如果元素是 運算式元素,則會叫用初始化實例 索引器,以在目前索引處加入評估的運算式。
    • 如果元素是 擴散元素,則會使用以下其中一項:
      • 會叫用知名介面或型別的成員,將項目從擴展元素表達式複製到初始化實例。
      • 散佈項目表達式 上叫用相關的 GetEnumerator 實例或延伸方法,對於枚舉器中的每個項目,會叫用初始化實例 的索引器,以在當前索引新增項目。 如果列舉器實作 IDisposable,則不論是否發生例外狀況,列舉完成後都會呼叫 Dispose
      • 在具有初始化實例和 int 索引做為自變數的 散佈項目表達式 上叫用適用的 CopyTo 實例或擴充方法。
  • 如果已為集合配置中繼記憶體,則會使用實際的集合長度配置集合實例,並將初始化實例中的值複製到集合實例,或者如果需要範圍,編譯程式 可能會 使用中繼記憶體中實際集合長度的範圍。 否則,初始化實例是集合實例。

  • 如果目標類型具有 create 方法,則會使用span實例叫用 create 方法。


注意: 編譯程式可能會在評估後續元素之後,延遲 將元素新增至集合,或 延遲 逐一查看散佈元素。 (當後續的散佈專案 可計算的 屬性時,允許在配置集合之前計算集合的預期長度。相反地,編譯程式可能會急切地 急切地 將元素加入集合中,而且當延遲沒有優勢時,急切地 逐一查看散佈元素。

請考慮下列集合表示式:

int[] x = [a, ..b, ..c, d];

如果展開元素 bc可計算的,編譯器可能會延遲從 ab 新增項目,直到評估了 c 之後,以便按預期的長度配置生成的數組。 之後,編譯程式可以在評估 d之前,樂意地從 c新增項目。

var __tmp1 = a;
var __tmp2 = b;
var __tmp3 = c;
var __result = new int[2 + __tmp2.Length + __tmp3.Length];
int __index = 0;
__result[__index++] = __tmp1;
foreach (var __i in __tmp2) __result[__index++] = __i;
foreach (var __i in __tmp3) __result[__index++] = __i;
__result[__index++] = d;
x = __result;

空集合字面值

  • 空常值 [] 沒有類型。 不過,類似於 null 常值,這個常值可以隱含地轉換成任何可建構 集合類型

    例如,下列內容不合法,因為沒有 目標類型,而且沒有涉及其他轉換:

    var v = []; // illegal
    
  • 可省略分散空常值。 例如:

    bool b = ...
    List<int> l = [x, y, .. b ? [1, 2, 3] : []];
    

    在這裡,如果 b 為 false,則不需要針對空集合表達式建構任何值,因為它會立即分散到最終常值中的零值。

  • 如果用來建構已知不可變動的最終集合值,則允許空白集合表達式成為單一集合。 例如:

    // Can be a singleton, like Array.Empty<int>()
    int[] x = []; 
    
    // Can be a singleton. Allowed to use Array.Empty<int>(), Enumerable.Empty<int>(),
    // or any other implementation that can not be mutated.
    IEnumerable<int> y = [];
    
    // Must not be a singleton.  Value must be allowed to mutate, and should not mutate
    // other references elsewhere.
    List<int> z = [];
    

參考安全

如需 安全內容 值的定義,請參閱 安全內容條件約束宣告區塊函式成員,以及 呼叫端內容

集合表示式 的安全環境 是:

  • 空集合表達式 [] 的安全背景是 呼叫端背景

  • 如果目標類型是 範圍類型System.ReadOnlySpan<T>,且 T原始類型之一,如boolsbytebyteshortushortcharintuintlongulongfloatdouble,並且集合表達式僅包含 常數值,則集合表達式的安全上下文是 呼叫者上下文

  • 如果目標類型是 範圍類型System.Span<T>System.ReadOnlySpan<T>,則集合表達式的安全內容是 宣告區塊

  • 如果目標類型是具有 create 方法ref 結構類型,則集合表達式的安全內容是 create 方法的調用 安全內容,其中集合表達式是方法的 span 自變數。

  • 否則,集合表達式的安全上下文為 呼叫端上下文

具有 宣告區塊安全內容的集合表達式 無法逸出封入範圍,編譯程式 可能會 將集合儲存在堆疊上,而不是堆積。

若要允許 ref 結構類型的集合表達式逸出 宣告區塊,可能需要將表達式轉換成另一個類型。

static ReadOnlySpan<int> AsSpanConstants()
{
    return [1, 2, 3]; // ok: span refers to assembly data section
}

static ReadOnlySpan<T> AsSpan2<T>(T x, T y)
{
    return [x, y];    // error: span may refer to stack data
}

static ReadOnlySpan<T> AsSpan3<T>(T x, T y, T z)
{
    return (T[])[x, y, z]; // ok: span refers to T[] on heap
}

類型推斷

var a = AsArray([1, 2, 3]);          // AsArray<int>(int[])
var b = AsListOfArray([[4, 5], []]); // AsListOfArray<int>(List<int[]>)

static T[] AsArray<T>(T[] arg) => arg;
static List<T[]> AsListOfArray<T>(List<T[]> arg) => arg;

類型推斷規則 已更新如下。

第一階段 的現有規則會擷取至新的 輸入類型推斷 區段,並將規則新增至 輸入類型推斷集合表達式的輸出類型推斷

11.6.3.2 第一個階段

針對每個方法自變數 Eᵢ

  • 輸入類型推斷對應到 參數類型

輸入類型推斷 是從表示式 ,以下列方式 類型

  • 如果 是集合運算式 ,包含元素 ,且 是具有 元素類型 的類型,或 是一個可為 Null 的實值類型 ,且 具有 元素類型,則每個
    • 如果 表示式專案,則會從輸入類型 推斷
    • 如果 是具有 反覆項目類型散佈 專案,則會從進行 下限 推斷。
  • [來自第一階段的現有規則]...

11.6.3.7 輸出類型推斷

輸出類型推斷 是從表示式 ,以下列方式 類型

  • 如果 是具有專案 集合運算式,且 是具有 項目類型可為 Null 的實值類型,且 具有 項目類型,則每個
    • 如果 表示式元素,則會將推斷成輸出類型
    • 如果 Eᵢ散佈專案,則不會從 Eᵢ進行推斷。
  • [輸出類型推斷的現有規則]...

擴充方法

擴充方法調用規則 沒有任何變更。

12.8.10.3 擴充方法的調用

擴充方法 Cᵢ.Mₑ 在以下情況下符合資格

  • ...
  • 隱含識別、參考或boxing轉換從 exprMₑ的第一個參數類型。

集合表達式沒有自然類型,因此 類型 的現有轉換不適用。 因此,集合表達式無法直接作為擴充方法調用的第一個參數。

static class Extensions
{
    public static ImmutableArray<T> AsImmutableArray<T>(this ImmutableArray<T> arg) => arg;
}

var x = [1].AsImmutableArray();           // error: collection expression has no target type
var y = [2].AsImmutableArray<int>();      // error: ...
var z = Extensions.AsImmutableArray([3]); // ok

重載解析

較佳的表達式轉換 會更新為偏好集合運算式轉換中的特定目標類型。

在更新的規則中:

  • span_type 是下列的其中一項:
    • System.Span<T>
    • System.ReadOnlySpan<T>
  • array_or_array_interface 是下列其中一項:
    • 一種陣列類型
    • 下列其中一個 介面類型陣列型態實作:
      • System.Collections.Generic.IEnumerable<T>
      • System.Collections.Generic.IReadOnlyCollection<T>
      • System.Collections.Generic.IReadOnlyList<T>
      • System.Collections.Generic.ICollection<T>
      • System.Collections.Generic.IList<T>

假設有一個隱含轉換 C₁ 會從表達式 E 轉換成類型 T₁,以及另一個隱含轉換 C₂ 會從表達式 E 轉換成類型 T₂,若下列條件之一成立,則 C₁ 是比 C₂ 更好的 轉換

  • E集合表達式,且下列條件之一成立:
    • T₁System.ReadOnlySpan<E₁>,而且 T₂System.Span<E₂>,而且隱含轉換會從 E₁E₂
    • T₁System.ReadOnlySpan<E₁>System.Span<E₁>,而 T₂ 是一個具有 元素類型E₂陣列或陣列接口,並且存在從 E₁E₂ 的隱含轉換。
    • T₁ 不是 span_type,而且 T₂ 不是 span_type,而且隱含的轉換會從 T₁ 轉換成 T₂
  • E 不是 集合表達式,以下其中之一成立:
    • E 完全符合 T₁,而 ET₂ 不完全相符
    • E 完全符合 T₁T₂或與兩者皆不符合,而 T₁ 是相比 T₂ 更好的 轉換目標
  • E 是方法群組...

陣列初始化運算式與集合運算式之間多載解析的差異範例:

static void Generic<T>(Span<T> value) { }
static void Generic<T>(T[] value) { }

static void SpanDerived(Span<string> value) { }
static void SpanDerived(object[] value) { }

static void ArrayDerived(Span<object> value) { }
static void ArrayDerived(string[] value) { }

// Array initializers
Generic(new[] { "" });      // string[]
SpanDerived(new[] { "" });  // ambiguous
ArrayDerived(new[] { "" }); // string[]

// Collection expressions
Generic([""]);              // Span<string>
SpanDerived([""]);          // Span<string>
ArrayDerived([""]);         // ambiguous

範圍類型

跨度類型 ReadOnlySpan<T>Span<T> 都是 可建構的集合類型。 它們的支持遵循 params Span<T>的設計原則。 具體來說,如果 params 陣列在編譯器設定的限制範圍內(如果有的話),建構任何一個範圍將會在 堆疊上產生陣列 T[]。 否則,陣列將會配置在堆上。

如果編譯程式選擇在堆疊上配置,就不需要直接將常值轉譯至該特定點的 stackalloc。 例如,假設:

foreach (var x in y)
{
    Span<int> span = [a, b, c];
    // do things with span
}

編譯程式只要確保 Span 意義維持不變,並維護 範圍安全性,即可使用 stackalloc 進行轉譯。 例如,它可以將上述轉譯為:

Span<int> __buffer = stackalloc int[3];
foreach (var x in y)
{
    __buffer[0] = a
    __buffer[1] = b
    __buffer[2] = c;
    Span<int> span = __buffer;
    // do things with span
}

如果可用,編譯程式也可以在選擇在堆疊上配置時,使用 內嵌數位。 請注意,在 C# 12 中,無法使用集合表達式初始化內嵌陣列。 這項功能是一個公開提案。

如果編譯程式決定在堆積上配置,則 Span<T> 的轉譯只是:

T[] __array = [...]; // using existing rules
Span<T> __result = __array;

集合常值轉譯

如果集合表達式中每個 散佈元素 的編譯時間類型 可計算,則集合表達式是 已知 長度的。

介面轉譯

不可變動的介面轉譯

假設目標類型不包含變動成員,也就是 IEnumerable<T>IReadOnlyCollection<T>IReadOnlyList<T>,則需要符合規範的實作,才能產生實作該介面的值。 如果一個類型是合成的,無論哪一種介面類型作為目標,都建議該合成類型實作所有這些介面,以及 ICollection<T>IList<T>。 這可確保與現有函式庫的最大相容性,包括那些通過檢查數值所實作的介面來啟用效能優化的函式庫。

此外,值必須實作非泛型 ICollectionIList 介面。 這可讓集合表達式在數據系結等案例中支持動態反省。

符合規範的實作是免費的:

  1. 請使用已實作必要介面的現有類型。
  2. 合成實作所需介面的類型。

不論是哪一種情況,都允許使用的型別實作比嚴格要求更大的介面集。

合成類型可以自由運用任何策略,只要能正確實作所需的介面即可。 例如,合成型別可能會直接內嵌本身內的元素,以避免需要額外的內部集合配置。 合成類型也無法使用任何記憶體,選擇直接計算值。 例如,傳回 index + 1 以對應 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  1. 針對 ICollection<T>.IsReadOnly 進行查詢時,此值必須傳回 true(如果已實作),且非泛型 IList.IsReadOnlyIList.IsFixedSize。 這可確保使用者可以適當地判斷集合是不可變動的,即使實作了可變動的視圖。
  2. 值必須在任何對變更方法的調用中拋出(例如 IList<T>.Add)。 這可確保安全性,防止非可變集合意外變動。

可變動的介面轉譯

指定包含可變動成員的目標類型,名稱為 ICollection<T>IList<T>

  1. 值必須是 List<T>的實例。

已知長度轉譯

擁有 已知長度 可讓您有效率地建構結果,而且結果中可能沒有複製數據,也不會有不必要的寬限空間。

沒有 已知的長度 不會阻止任何結果的創建。 不過,這可能會導致在生成數據並移至最終目的地時,增加額外的CPU和記憶體成本。

  • 對於 已知長度 常值 [e1, ..s1, etc],翻譯首先會以下列內容開始:

    int __len = count_of_expression_elements +
                __s1.Count;
                ...
                __s_n.Count;
    
  • 給定該字面值的目標類型 T

    • 如果 T 是某些 T1[],則此字面值被翻譯為:

      T1[] __result = new T1[__len];
      int __index = 0;
      
      __result[__index++] = __e1;
      foreach (T1 __t in __s1)
          __result[__index++] = __t;
      
      // further assignments of the remaining elements
      

      允許實作使用其他方法來填入陣列。 例如,使用有效率的大量複製方法,例如 .CopyTo()

    • 如果 T 是某些 Span<T1>,則字面量的翻譯與上述相同,唯一不同的是 __result 的初始化被翻譯為:

      Span<T1> __result = new T1[__len];
      
      // same assignments as the array translation
      

      如果範圍安全性能夠維持,翻譯可能會使用stackalloc T1[]內嵌陣列,而不是new T1[]

    • 如果 T 是一些 ReadOnlySpan<T1>,則常值會轉譯為與 Span<T1> 案例相同的常值,但最終結果會是 Span<T1>隱含 地轉換成 ReadOnlySpan<T1>

      ReadOnlySpan<T1>,其中 T1 是某種基本類型,並且所有集合元素都是常數,因此其數據不需要儲存在堆積或堆疊上。 例如,實作可以直接將此範圍建構為程序數據區段部分的參考。

      上述形式(適用於陣列和跨度)是集合表達式的基底表示法,並用於下列轉換規則:

      • 如果 T 是某些具有對應 創建方法B.M<U0, U1, …>()C<S0, S1, …>,則該常值會被轉譯為:

        // Collection literal is passed as is as the single B.M<...>(...) argument
        C<S0, S1, …> __result = B.M<S0, S1, …>([...])
        

        由於 create 方法 必須具有某些具現化 ReadOnlySpan<T>的自變數類型,因此,將集合表達式傳遞至 create 方法時,會套用 spans 的轉譯規則。

      • 如果 T 支援 集合初始化器,則:

        • 如果類型 T 包含一個帶有單一參數 int capacity的可存取建構函式,則文本字面值會被轉譯為:

          T __result = new T(capacity: __len);
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          注意:參數的名稱必須 capacity

          此表單允許字面值指定新建構類型中的元素數量,以便能有效配置內部存儲。 這樣可避免在新增元素時重新配置的浪費。

        • 否則,文字會被轉譯為:

          T __result = new T();
          
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          這允許創建目標類型,儘管無法進行容量最佳化,以免內部存儲重新分配。

未知的長度轉譯

  • 在給定 未知長度 常值的情況下,指定目標類型 T

    • 如果 T 支援 集合初始化運算式,則字面值會轉譯為:

      T __result = new T();
      
      __result.Add(__e1);
      foreach (var __t in __s1)
          __result.Add(__t);
      
      // further additions of the remaining elements
      

      這允許散佈任何可反覆運算的類型,儘管可能優化量最少。

    • 如果 T 是一些 T1[],則文字具有與以下相同的語義:

      List<T1> __list = [...]; /* initialized using predefined rules */
      T1[] __result = __list.ToArray();
      

      不過,上述項目沒有效率;它會建立中繼清單,然後從中建立最終陣列的複本。 實作可以自由地將此優化,例如產生如下的程序代碼:

      T1[] __result = <private_details>.CreateArray<T1>(
          count_of_expression_elements);
      int __index = 0;
      
      <private_details>.Add(ref __result, __index++, __e1);
      foreach (var __t in __s1)
          <private_details>.Add(ref __result, __index++, __t);
      
      // further additions of the remaining elements
      
      <private_details>.Resize(ref __result, __index);
      

      這可減少浪費和複製,而不會造成圖書館收藏可能帶來的額外負擔。

      傳遞至 CreateArray 的數值可用來提供初始大小提示,以防止浪費的大小調整。

    • 如果 T 是一些 範圍類型,則實作可能會遵循上述 T[] 策略,或任何其他具有相同語意但效能較佳的策略。 例如,CollectionsMarshal.AsSpan(__list) 可用來直接取得跨度值,而不是將陣列配置為清單元素的複本。

不支援的場景

雖然集合常值可用於許多情境,但有一些情境是它們無法取代的。 這些包括:

  • 多維度陣列(例如 new int[5, 10] { ... })。 沒有包含維度的功能,而且所有集合常量要麼是線性結構,要麼是映射結構。
  • 將特殊值傳遞給建構函式的集合。 沒有任何機制可存取正在使用的建構函數。
  • 巢狀集合初始化器,例如 new Widget { Children = { w1, w2, w3 } }。 此表單需要保留,因為它具有與 Children = [w1, w2, w3]截然不同的語意。 前者會在 .Children 上重複呼叫 .Add,而後者則會在 .Children上指派新的集合。 如果無法指派 .Children,我們可以考慮退而求其次,將後一種形式新增至現有的集合,但這樣做似乎可能會極為混淆。

語法模棱兩可

  • 有兩個「真正的」語法模稜兩可,其中有多種合法語法解釋是針對使用 collection_literal_expression的程式碼。

    • spread_elementrange_expression模棱兩可。 理論上可以有:

      Range[] ranges = [range1, ..e, range2];
      

      若要解決此問題,我們可以:

      • 要求使用者將 (..e) 括在括號中,或如果需要範圍,則包含起始索引 0..e
      • 選擇不同的語法來處理擴展(例如 ...)。 這很不幸,因為切片模式缺乏一致性。
  • 有兩種情況,雖然沒有真正的模棱兩可,但語法大幅增加了剖析的複雜度。 雖然在工程時間充分的情況下不是問題,但查看程式碼時,這仍然會增加使用者的認知負擔。

    • 語句或區域函式上 collection_literal_expressionattributes 之間的模棱兩可。 考慮:

      [X(), Y, Z()]
      

      這可能是下列其中一項:

      // A list literal inside some expression statement
      [X(), Y, Z()].ForEach(() => ...);
      
      // The attributes for a statement or local function
      [X(), Y, Z()] void LocalFunc() { }
      

      如果沒有複雜的前瞻,就不可能在不消耗整個字面值的情況下分辨出來。

      要解決此問題的選項包括:

      • 允許這樣做,執行剖析工作來判斷這其中哪一種情況。
      • 不允許這樣做,並要求使用者將常值包裝在括弧中,例如 ([X(), Y, Z()]).ForEach(...)
      • conditional_expression 中的 collection_literal_expressionnull_conditional_operations之間的模棱兩可。 考慮:
      M(x ? [a, b, c]
      

      這可能是下列其中一項:

      // A ternary conditional picking between two collections
      M(x ? [a, b, c] : [d, e, f]);
      
      // A null conditional safely indexing into 'x':
      M(x ? [a, b, c]);
      

      如果沒有複雜的向前查看,在未消耗完整個文字的情況下是無法確定的。

      注意:即使沒有 自然類型,這也會是個問題,因為目標類型會透過 conditional_expressions套用。

      和其他人一樣,我們可能需要括弧來釐清。 換句話說,預設為 null_conditional_operation 解譯,除非撰寫為:x ? ([1, 2, 3]) :。 然而,這似乎相當不幸。 這類程式碼看起來合理,但可能會讓人困惑。

缺點

  • 這在我們已經擁有的眾多方式之上,為集合表達式引入了 的另一種 形式。 這是語言的額外複雜度。 換句話說,這也使得可以統一一個 語法來統一所有規則,這表示現有程式碼可以簡化並移動到統一的外觀。
  • 使用 [...] 而不是 {...},這偏離了我們通常為陣列和集合初始化所使用的語法。 具體而言,它會使用 [...] 而不是 {...}。不過,當我們執行清單模式時,語言小組已經解決了這個問題。 我們嘗試讓 {...} 與清單模式一起運作,但遇到了無法克服的問題。 因此,我們移至 [...],這對 C# 而言是新的,但在許多程式設計語言中感覺很自然,讓我們能夠從頭開始,沒有任何模糊。 使用 [...] 作為對應常值形式與我們的最新決策互補,並讓我們有一個乾淨的工作環境,不會出現問題。

這確實會將缺陷引入語言。 例如,下列同時是合法的而且(幸運的是)代表完全相同的意思:

int[] x = { 1, 2, 3 };
int[] x = [ 1, 2, 3 ];

不過,鑒於新字面語法帶來的廣度和一致性,我們應該考慮建議人們轉向新的形式。 IDE 建議和修正有助於這一點。

替代方案

  • 其他哪些設計被考慮過? 不這樣做的影響是什麼?

已解決的問題

  • 內嵌數位列 無法使用,且 反覆運算類型 為基本類型時,編譯程式是否應該使用 stackalloc 進行堆疊配置?

    決議:否。 管理 stackalloc 緩衝區需要比 內嵌陣列更多的工作, 以確保當集合表達式位於迴圈中時,緩衝區不會被重複配置。 編譯程式和產生的程式代碼中的額外複雜度,超過舊平臺上堆棧配置的優點。

  • 相較於 Length/Count 屬性的評估,我們應該以何種順序來評估字面元素? 我們應該先評估所有元素,然後再評估所有長度嗎? 或者,我們應該先評估一個元素,再評估其長度,然後再評估下一個元素,如此循環下去?

    解決方案:我們會先評估所有元素,然後再評估其他所有元素。

  • 未知長度 常值是否可以建立需要 已知長度的集合類型,例如陣列、範圍或 Construct(array/span) 集合? 這很難有效率地執行,但可能透過巧妙地使用集區陣列和/或產生器。

    解決方案:是的,我們允許從 未知長度 常值建立固定長度集合。 編譯程式可以盡可能有效率的方式實作此作業。

    下列文字可用來記錄本主題的原始討論。

    使用者一律可以將 未知長度 常值變成 已知長度, 程式代碼如下:

    ImmutableArray<int> x = [a, ..unknownLength.ToArray(), b];
    

    不過,這很不幸,因為需要強制配置暫存空間。 如果我們控制發出的方式,我們可能會更有效率。

  • collection_expression 是否可以將目標型別設為 IEnumerable<T> 或其他集合介面?

    例如:

    void DoWork(IEnumerable<long> values) { ... }
    // Needs to produce `longs` not `ints` for this to work.
    DoWork([1, 2, 3]);
    

    解決方案:是,實值可以被作為 List<T> 所實作的任何介面類型 I<T> 的目標型別。 例如,IEnumerable<long>。 這就如同將List<long>作為目標類型進行轉換,然後將該結果賦值給指定的介面類型。 下列文字可用來記錄本主題的原始討論。

    此處的開放問題是判斷要實際建立的基礎類型。 其中一個選項是查看 params IEnumerable<T>的提案。 在那裡,我們會產生一個陣列來傳遞這些值,類似於使用 params T[]時所發生的情況。

  • 編譯程式能否/是否應針對 []發出 Array.Empty<T>()? 我們是否應該要求它這樣做,以為了盡量避免分配?

    是的。 編譯程式應該針對任何合法情況發出 Array.Empty<T>(),而最終結果為不可變動。 例如,以 T[]IEnumerable<T>IReadOnlyCollection<T>IReadOnlyList<T>為目標。 當目標為可變動時,不應使用 Array.Empty<T>ICollection<T>IList<T>)。

  • 我們是否應該擴充集合初始化器來尋找非常常見的 AddRange 方法? 底層結構化類型可以使用它來執行可能更有效地添加展開元素。 我們也可能會想要尋找像 .CopyTo 這樣的東西。 這裡可能會有缺點,因為這些方法最終可能會導致過度的配置/分派,而不是直接列舉在翻譯的程序代碼中。

    是的。 實作允許利用其他方法來初始化集合值,並假設這些方法具有妥善定義的語意,而且該集合類型應該「運作良好」。 不過,在實務上,實作應該謹慎,因為一種方式(大量複製)可能會帶來負面後果(例如,將結構集合進行封箱)。

    實作應在沒有缺點的情況下善加利用。 例如可以使用 .AddRange(ReadOnlySpan<T>) 方法。

未解決的問題

  • 迭代類型 被某種定義視為「模棱兩可」時,我們應該允許推斷 元素類型 嗎? 例如:
Collection x = [1L, 2L];

// error CS1640: foreach statement cannot operate on variables of type 'Collection' because it implements multiple instantiations of 'IEnumerable<T>'; try casting to a specific interface instantiation
foreach (var x in new Collection) { }

static class Builder
{
    public Collection Create(ReadOnlySpan<long> items) => throw null;
}

[CollectionBuilder(...)]
class Collection : IEnumerable<int>, IEnumerable<string>
{
    IEnumerator<int> IEnumerable<int>.GetEnumerator() => throw null;
    IEnumerator<string> IEnumerable<string>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}
  • 建立並立即編製集合常值索引是否合法? 注意:這需要回答下列未解決的問題:集合常值是否有 自然類型

  • 大型集合的堆疊記憶體配置可能會導致堆疊溢位。 編譯器是否應該具有啟發式方法,以便將此數據放在堆上? 是否應該未指定語言以允許此彈性? 我們應該遵循 params Span<T>的規格。

  • 我們需要將 spread_element設定為指定類型嗎? 例如,請考慮:

    Span<int> span = [a, ..b ? [c] : [d, e], f];
    

    注意:這可能常以以下形式出現,以允許在條件為真時包含某些元素,而若條件為假則不包含任何元素。

    Span<int> span = [a, ..b ? [c, d, e] : [], f];
    

    為了評估這個完整字面值,我們需要評估其中的元素表達式。 這表示能夠評估 b ? [c] : [d, e]。 如果缺乏一個目標型別來在特定上下文中評估此表達式,並且沒有任何種類的 自然類型,我們將無法決定應該如何處理這裡的 [c][d, e]

    為了解決此問題,我們可以說,在評估常值 spread_element 表達式時,隱含了與常值本身目標類型相同的目標類型。 因此,在上述內容中,這會重寫為:

    int __e1 = a;
    Span<int> __s1 = b ? [c] : [d, e];
    int __e2 = f;
    
    Span<int> __result = stackalloc int[2 + __s1.Length];
    int __index = 0;
    
    __result[__index++] = a;
    foreach (int __t in __s1)
      __result[index++] = __t;
    __result[__index++] = f;
    
    Span<int> span = __result;
    

使用 create 方法的可建構 集合類型的 規格, 對分類轉換的內容很敏感

在這種情況下,轉換的存在取決於 反覆運算類型 的概念,以及 集合類型。 如果存在一個採用 ReadOnlySpan<T>建構方法,其中 T迭代類型,則該轉換存在。 否則,則不會。

不過,疊代類型 會對執行 foreach 的上下文很敏感。 針對相同的 集合類型, 可能會因範圍內的擴充方法而有所不同,也可能未定義。

當類型不是設計成可foreach的時,這就符合foreach的目的。 如果是這樣的話,無論在何種情境下,擴充方法都無法變更類型被逐一遍歷的方式。

不過,轉換若會因情境而改變,這種感覺有點奇怪。 實際上,轉換是「不穩定」。 集合類型 明確設計為 可建構的, 可以省略非常重要的細節定義—其 迭代類型。 將類型保留為「不可轉換」本身。

以下是範例:

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

[CollectionBuilder(typeof(MyCollectionBuilder), nameof(MyCollectionBuilder.Create))]
class MyCollection
{
}
class MyCollectionBuilder
{
    public static MyCollection Create(ReadOnlySpan<long> items) => throw null;
    public static MyCollection Create(ReadOnlySpan<string> items) => throw null;
}

namespace Ns1
{
    static class Ext
    {
        public static IEnumerator<long> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                long s = l;
            }
        
            MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                               2];
        }
    }
}

namespace Ns2
{
    static class Ext
    {
        public static IEnumerator<string> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                string s = l;
            }
        
            MyCollection x1 = ["a",
                               2]; // error CS0029: Cannot implicitly convert type 'int' to 'string'
        }
    }
}

namespace Ns3
{
    class Program
    {
        static void Main()
        {
            // error CS1579: foreach statement cannot operate on variables of type 'MyCollection' because 'MyCollection' does not contain a public instance or extension definition for 'GetEnumerator'
            foreach (var l in new MyCollection())
            {
            }
        
            MyCollection x1 = ["a", 2]; // error CS9188: 'MyCollection' has a CollectionBuilderAttribute but no element type.
        }
    }
}

假設目前的設計,如果類型未定義 反覆運算類型本身,編譯程式就無法可靠地驗證 CollectionBuilder 屬性的應用程式。 如果我們不知道 反覆運算類型,我們就不知道 建立方法的簽名應該是什麼。 如果 迭代類型 源自情境,那麼不保證該類型會始終用於類似的情境中。

Params 集合 功能也會受到此影響。 無法在宣告點可靠地預測 params 參數的元素類型會讓人感到奇怪。 目前的提案還要求確保 create 方法 的存取性至少不低於 params集合類型。 除非 集合類型 定義其 反覆運算類型 本身,否則不可能以可靠的方式執行這項檢查。

請注意,我們也已針對編譯程式開啟 https://github.com/dotnet/roslyn/issues/69676,這基本上會觀察相同的問題,但從優化的觀點來加以討論。

建議

需要一種類型,此類型使用 CollectionBuilder 屬性來在其自身上定義其 迭代類型。 換句話說,這表示類型應該實作 IEnumarable/IEnumerable<T>,或者應該具有具有正確簽章的公用 GetEnumerator 方法(這不包括任何擴充方法)。

此外,現在 建立方法 需要「能在使用集合表達式的位置存取」。 這是另一個基於可及性的情境依賴性點。 此方法的用途與使用者定義轉換方法的用途非常類似,而且必須公開。 因此,我們也應該考慮要求 建立方法 為公用。

結論

LDM-2024-01-08 核准,但經過修改

反覆項目類型 的概念未在 轉換中一致地應用

  • 要實作 System.Collections.Generic.IEnumerable<T>結構類別 類型:
    • 針對每個 項目,Ei隱含轉換T

看起來假設 T 必須是 結構反覆項目類型,或者在此情況下是 類別類型。 不過,假設不正確。 這可能會導致非常奇怪的行為。 例如:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public void Add(string l) => throw null;
    
    public IEnumerator<string> GetEnumerator() => throw null; 
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
        
        MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                           2];
        MyCollection x2 = new MyCollection() { "b" };
    }
}
  • 若要實作 System.Collections.IEnumerable結構類別 類型,則不會實作System.Collections.Generic.IEnumerable<T>

實作看起來假設 迭代類型object,但規格沒有說明這個事實,並且也不要求每個 元素 必須轉換為任何東西。 不過,一般而言,反覆運算類型 不需要 object 類型。 您可以在下列範例中觀察到:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    public IEnumerator<string> GetEnumerator() => throw null; 
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
    }
}

反覆運算類型 的概念,是 Params 集合 功能的基礎。 這個問題會導致這兩個特徵之間出現奇怪的差異。 例如:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 

    public void Add(long l) => throw null; 
    public void Add(string l) => throw null; 
}

class Program
{
    static void Main()
    {
        Test("2"); // error CS0029: Cannot implicitly convert type 'string' to 'long'
        Test(["2"]); // error CS1503: Argument 1: cannot convert from 'collection expressions' to 'string'
        Test(3); // error CS1503: Argument 1: cannot convert from 'int' to 'string'
        Test([3]); // Ok

        MyCollection x1 = ["2"]; // error CS0029: Cannot implicitly convert type 'string' to 'long'
        MyCollection x2 = [3];
    }

    static void Test(params MyCollection a)
    {
    }
}
using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 
    public void Add(object l) => throw null;
}

class Program
{
    static void Main()
    {
        Test("2", 3); // error CS1503: Argument 2: cannot convert from 'int' to 'string'
        Test(["2", 3]); // Ok
    }

    static void Test(params MyCollection a)
    {
    }
}

不管選擇哪一種方式,對齊可能會很好。

建議

指定 結構類別類型 的可轉換性,以實作 反覆運算類型,而且每個 專案 隱含轉換

結論

已核准 LDM-2024-01-08

集合表達式轉換 是否需要具備一組最小的 API 來實現建構?

根據 轉換看似可建構的 集合類型實際上可能無法建構,這可能導致一些意想不到的多載解析行為。 例如:

class C1
{
    public static void M1(string x)
    {
    }
    public static void M1(char[] x)
    {
    }
    
    void Test()
    {
        M1(['a', 'b']); // error CS0121: The call is ambiguous between the following methods or properties: 'C1.M1(string)' and 'C1.M1(char[])'
    }
}

不過,『C1。M1(string)' 不是可供使用的候選項目,因為:

error CS1729: 'string' does not contain a constructor that takes 0 arguments
error CS1061: 'string' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)

以下是另一個具有使用者定義型別的範例,並且甚至未提及任何有效候選者的更嚴重錯誤:

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(C1 x)
    {
    }
    public static void M1(char[] x)
    {
    }

    void Test()
    {
        M1(['a', 'b']); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
    }

    public static implicit operator char[](C1 x) => throw null;
    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

情況看起來與我們過去在方法群組用於委派轉換的經驗非常相似。 也就是說,有一些情境中雖然存在轉換,但出現了錯誤。 我們決定確保,只要轉換出錯,就不會存在,以此來改善。

注意,使用「Params 集合」功能時,可能會遇到類似的問題。 最好不允許對不可建構的集合使用 params 修飾詞。 不過,在目前的提案中,檢查是以 轉換 區段為基礎。 以下是範例:

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(params C1 x) // It is probably better to report an error about an invalid `params` modifier
    {
    }
    public static void M1(params ushort[] x)
    {
    }

    void Test()
    {
        M1('a', 'b'); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
        M2('a', 'b'); // Ok
    }

    public static void M2(params ushort[] x)
    {
    }

    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

看起來問題先前已稍微討論過,請參閱 https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions。 當時有一個論點指出,規則目前的設定方式與插值字串處理器的指定方式一致。 以下是一則引用:

特別是,插入字串處理程式原本是這樣指定,但我們在考慮此問題之後修改了規格。

雖然有一些相似之處,但也有值得考慮的重要區別。 以下是來自 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion的引用:

類型 T 如果被賦予 System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute屬性,則其為 applicable_interpolated_string_handler_type。 有一個隱含 interpolated_string_handler_conversion,從 interpolated_string_expression,或者是完全由 插值字串表達式所組成並且只使用 運算符的 加法表達式

目標類型必須具有特殊屬性,這是表明類型意圖用作插入字串處理程式的強指標。 假設屬性的存在不是巧合,這是公平的。 相反地,類型為「可列舉」的事實不一定表示作者有意圖讓該類型可以被建構。 不過,create 方法的存在,在 集合類型上以 [CollectionBuilder(...)] 屬性表示,這明顯表示作者有意要此類型為可建構型別。

建議

針對實作 System.Collections.IEnumerable結構類別 類型,且沒有 建立方法轉換 區段,至少需要有下列 API:

  • 無參數的可用建構函式。
  • 可存取的 Add 實例或擴充方法,可透過 反覆項目類型的值來叫用, 做為自變數。

為了 Params Collectons 功能的目的,當這些 API 宣告為公用且是實例(與擴充)方法時,這類類型是有效的 params 類型。

結論

已獲得修改批准 LDM-2024-01-10

設計會議

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-01.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-09.md#ambiguity-of--in-collection-expressions https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-28.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-08.md https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-10.md

工作組會議

https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-06.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-14.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-21.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-05.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-28.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-05-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-12.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-03.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-10.md

即將進行的議程事項

  • 大型集合的堆疊分配可能會導致堆疊溢位。 編譯程式是否應該具有啟發式方法,以便將此數據放在堆疊上? 是否應該未指定語言以允許此彈性? 我們應該遵循spec/impl對 params Span<T>所做的工作。 選項包括:

    • 一律使用 stackalloc。 教人們注意 Span。 這使得 Span<T> span = [1, 2, ..s] 等功能能夠運作,只要 s 很小就無妨。 如果這可能會導致堆疊溢出,使用者可以建立一個陣列來替代,然後獲取此陣列的 span。 這似乎最符合人們可能想要的,但具有極端危險。
    • 只有在常值具有固定 個元素數目 stackalloc (亦即沒有散佈元素)。 這樣可能會讓事情一律安全,使用固定堆疊,而編譯程式(希望)能夠重複使用該固定緩衝區。 不過,這表示即使使用者知道在執行期間是完全安全的,諸如 [1, 2, ..s] 之類的操作仍然是不可能的。
  • 多載解析如何運作? 如果 API 具有:

    public void M(T[] values);
    public void M(List<T> values);
    

    M([1, 2, 3])會怎麼樣? 我們可能需要為這些轉換定義「優劣」。

  • 我們應該擴充集合初始化表達式來尋找非常常見的 AddRange 方法嗎? 基礎建構型別可以利用它來執行更有效率地新增散佈元素。 我們也可能想要尋找類似 .CopyTo 的項目。 這裡可能會有缺點,因為這些方法最終可能會導致過度的配置/分派,而不是直接列舉在翻譯的程序代碼中。

  • 泛型型別推斷應該更新為將類型資訊流向集合常值或從集合常值流出。 例如:

    void M<T>(T[] values);
    M([1, 2, 3]);
    

    這似乎很自然,這應該是推斷演算法可以察覺到的。 一旦支援 『base』 可建構的集合類型案例(T[]I<T>Span<T>new T()),則也應該脫離 Collect(constructible_type) 案例。 例如:

    void M<T>(ImmutableArray<T> values);
    M([1, 2, 3]);
    

    在這裡,Immutable<T> 可透過 init void Construct(T[] values) 方法來建構。 因此,T[] values 類型會用於對 [1, 2, 3] 的推斷,從而對 T推斷出 int

  • 轉換/索引模棱兩可。

    今天,以下是已編製索引的表達式

    var v = (Expr)[1, 2, 3];
    

    但是,能夠做如下的事情會很好:

    var v = (ImmutableArray<int>)[1, 2, 3];
    

    我們可以/應該在這裡休息嗎?

  • ?[相關的語法模棱兩可。

    nullable index access 的規則變更為規定 ?[之間不能有空格,這可能是值得考慮的。 這將是一個不相容的變更(但可能影響不大,因為如果你輸入它們並加上空格,VS 已經會自動將它們合併)。 如果我們這樣做,我們就可以將 x?[y] 解析得與 x ? [y]不同。

    如果我們想要使用 https://github.com/dotnet/csharplang/issues/2926,就會發生類似的情況。 在該世界中,x?.yx ? .y模棱兩可。 如果我們需要 ?. 緊接,我們可以從語法上輕鬆區分這兩種情況。