共用方式為


params Collections

注意

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

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

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

冠軍問題:https://github.com/dotnet/csharplang/issues/7700

總結

在 C# 12 語言中,新增了建立集合類型實例的支援,而不只是陣列。 請參閱 集合表達式。 此提案會將 params 支援延伸至所有這類集合類型。

動機

params 陣列參數提供一種方便的方式,用於呼叫接受任意長度參數清單的方法。 今天 params 參數必須是陣列類型。 不過,當呼叫採用其他集合類型的 API 時,開發人員可能會享有類似的方便性。 例如,ImmutableArray<T>ReadOnlySpan<T>或純 IEnumerable。 特別是在編譯器能避免隱含陣列配置以建立集合時(ImmutableArray<T>ReadOnlySpan<T>等)。

目前,在 API 採用集合類型的情況下,開發人員通常會新增接受陣列的 params 多載,建構目標集合並使用該集合呼叫原始多載,因此 API 使用者必須進行額外的陣列配置,以求便利。

另一個動機是能夠新增參數範圍超載,只要重新編譯現有的原始程式碼,它就會優先於陣列版本。

詳細設計

方法參數

方法參數 區段會依照下列方式進行調整。

formal_parameter_list
    : fixed_parameters
-    | fixed_parameters ',' parameter_array
+    | fixed_parameters ',' parameter_collection
-    | parameter_array
+    | parameter_collection
    ;

-parameter_array
+parameter_collection
-    : attributes? 'params' array_type identifier
+    : attributes? 'params' 'scoped'? type identifier
    ;

parameter_collection 包含一組選擇性的 屬性params 修飾詞、選擇性 scoped 修飾詞、類型,以及 標識符。 參數集合會宣告具有指定名稱之指定型別的單一參數。 參數集合的 類型 應該是下列集合表達式的有效目標類型之一(請參閱 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md#conversions):

  • 單一維度 陣列類型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

在方法調用中,參數集合允許指定具有給定參數類型的單一參數,或是允許指定零個或多個具有集合的 元素類型的參數。 參數集合會在 參數集合中進一步說明,

parameter_collection 可以出現在選擇性參數之後,但不能有預設值;如果省略了 parameter_collection 的引數,則會建立一個空集合。

參數集合

參數位列 區段會重新命名並調整,如下所示。

params 修飾詞宣告的參數是參數集合。 如果正式參數清單包含參數集合,則它應該是清單中的最後一個參數,且其類型應指定於 Method 參數 區段。

附註:無法將修飾詞 paramsinoutref結合。 尾注

參數集合允許在方法調用的兩種方式之一中指定自變數:

  • 為參數集合指定的自變數可以是可隱含轉換成參數集合類型的單一表達式。 在此情況下,參數集合的行為與實值參數類似。
  • 或者,調用可以指定參數集合的零個或多個自變數,其中每個自變數都是可隱含轉換成參數集合 項目類型的表達式。 在這種情況下,調用會依據 集合表達式中指定的規則來創建參數集合類型的實例, 就好像引數是以相同順序作為集合表達式中的表達式元素,並將新創建的集合實例作為實際引數使用。 建構集合實例時,會使用原始 未轉換 自變數。

除了允許在調用中使用可變數量的參數之外,參數集合在功能上與相同類型的值參數完全相同。

執行多載決議時,具有參數集合的方法無論是通常形式還是展開形式,都可能適用。 只有在方法的一般形式不適用,以及相同類型中尚未宣告與展開形式相同簽名的適用方法時,才能使用方法的展開形式。

當方法可以當做參數集合本身以及同時做為參數集合的元素時,使用單一參數集合自變數,在方法的一般形式和展開形式之間,就會產生潛在的模棱兩可。 不過,模棱兩可沒有問題,因為可以視需要插入轉換或使用集合表達式來解決。

簽章和多載

params 修飾詞在 簽章和 重載中的所有規則維持原樣。

適用的功能成員

適用的函式成員 區段依照下列方式調整。

如果包含參數集合的函式成員不適用於其一般形式,則函式成員可能會適用於其 展開形式

  • 如果參數集合不是陣列,展開的窗體不適用於語言版本 C# 12 和以下版本。
  • 展開形式是藉由將函式成員宣告中的參數集合,依據其 元素類型,替換為零個或多個值參數,使得自變數清單中的自變數數量 A 與參數總數相符。 如果 A 自變數比函數成員宣告中的固定參數數目少,則無法建構函式成員的展開形式,因此不適用。
  • 否則,如果 A中的每個參數符合以下任一條件,則適用展開形式:
    • 自變數的參數傳遞模式與對應參數的參數傳遞模式相同,以及
      • 針對固定值參數或擴充所建立的 value 參數,從引數表示式隱含轉換到對應參數的類型,或
      • 對於 inoutref 參數,自變數表達式的類型與對應參數的類型相同。
    • 自變數的參數傳遞模式是值,而對應參數的參數傳遞模式是輸入,而且從自變數表達式到對應參數類型的隱含轉換存在

更佳的函數成員

Better 函式成員 區段的調整方式如下。

假設自變數清單 具有一組自變數表達式 和兩個適用的函式成員 ,並使用參數類型 定義為比 更好的函式成員

  • 針對每個參數,從 EᵥQᵥ 的隱含轉換不比從 EᵥPᵥ的隱含轉換更好,且
  • 對於至少一個自變數,從 Eᵥ 轉換成 Pᵥ 比從 Eᵥ 轉換成 Qᵥ更好。

如果參數類型序列 {P₁, P₂, ..., Pᵥ}{Q₁, Q₂, ..., Qᵥ} 相等(也就是每個 Pᵢ 都有對應 Qᵢ的身分識別轉換),則會套用下列中斷系結規則,以判斷更好的函式成員。

  • 如果 Mᵢ 是非泛型方法,而且 Mₑ 是泛型方法,則 Mᵢ 優於 Mₑ
  • 否則,如果 Mᵢ 適用於其一般形式,且 Mₑ 具有參數集合,且僅適用於其展開形式,則 Mᵢ 優於 Mₑ
  • 否則,如果這兩種方法都有參數集合,而且僅適用於其展開形式,而且如果 Mᵢ 的 params 集合比 Mₑ的 params 集合少,則 MᵢMₑ更好。
  • 否則,如果 Mᵥ 具有比 Mₓ更明確的參數類型,則 MᵥMₓ更好。 讓 {R1, R2, ..., Rn}{S1, S2, ..., Sn} 代表 MᵥMₓ的未經驗證和未展開的參數類型。 如果每個參數的 Mᵥ 不低於 Mₓ,且至少一個參數的 RxSx更具體,那麼 Rx的參數類型比 Sx的參數類型更具體:
    • 類型參數比非類型參數更不明確。
    • 遞歸來看,一個建構類型比另一個建構類型更具體(類型參數數量相同),如果至少有一個類型參數更具體,且沒有任何類型參數比另一個中的對應類型參數更不具體。
    • 如果第一個陣列類型的元素類型比第二個的元素類型更具體,那麼(維度數目相同的情況下)前者的陣列類型會比後者的更具體。
  • 否則,如果一個成員是非提升運算符,另一個是提升運算符,則非提升的運算符會更好。
  • 如果兩個函式成員都找不到較佳,而且 Mᵥ 的所有參數都有對應的自變數,而預設自變數則至少要取代 Mₓ中的一個選擇性參數,則 MᵥMₓ更好。
  • 如果至少有一個參數 Mᵥ 使用 的參數傳遞選擇§12.6.4.4),比 Mₓ 中的對應參數更好,而 Mₓ 中沒有任何參數使用比 Mᵥ更好的參數傳遞選擇,那麼 MᵥMₓ更好。
  • 否則,如果兩個方法都有參數集合,而且僅適用於其展開形式,則若相同的參數集合對應至這兩個方法的集合元素,MᵢMₑ 更好,並且以下條件之一成立(這對應至 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-13.0/collection-expressions-better-conversion.md):
    • 兩個參數集合都不是 span_type,而且 Mᵢ 的 params 集合到 Mₑ 的 params 集合存在隱含轉換
    • Mᵢ參數集合是 System.ReadOnlySpan<Eᵢ>,而 Mₑ 的參數集合是 System.Span<Eₑ>,而且從 EᵢEₑ的識別轉換存在
    • Mᵢ參數集合是 System.ReadOnlySpan<Eᵢ>System.Span<Eᵢ>,而 Mₑ 的 params 集合是具有 元素類型Eₑ,且識別轉換會從 EᵢEₑ
  • 否則,沒有哪個函式成員更優秀。

將新的決勝規則放在清單結尾的原因是因為最後一個子項目

  • 兩個參數集合都不是 span_type,而且 Mᵢ 的 params 集合到 Mₑ 的 params 集合存在隱含轉換

它適用於陣列,因此,提前執行加權將會對現有情境的行為產生變更。

例如:

class Program
{
    static void Main()
    {
        Test(1);
    }

    static void Test(in int x, params C2[] y) {} // There is an implicit conversion from `C2[]` to `C1[]`
    static void Test(int x, params C1[] y) {} // Better candidate because of "better parameter-passing choice"
}

class C1 {}
class C2 : C1 {}

如果任何先前的中斷系結規則都適用(包括「更好的自變數轉換」規則),則當明確集合表達式改用為自變數時,多載解析結果可能會有所不同。

例如:

class Program
{
    static void Test1()
    {
        M1(['1', '2', '3']); // IEnumerable<char> overload is used because `char` is an exact match
        M1('1', '2', '3');   // IEnumerable<char> overload is used because `char` is an exact match
    }

    static void M1(params IEnumerable<char> value) {}
    static void M1(params System.ReadOnlySpan<MyChar> value) {}

    class MyChar
    {
        private readonly int _i;
        public MyChar(int i) { _i = i; }
        public static implicit operator MyChar(int i) => new MyChar(i);
        public static implicit operator char(MyChar c) => (char)c._i;
    }

    static void Test2()
    {
        M2([1]); // Span overload is used
        M2(1);   // Array overload is used, not generic
    }

    static void M2<T>(params System.Span<T> y){}
    static void M2(params int[] y){}

    static void Test3()
    {
        M3("3", ["4"]); // Ambiguity, better-ness of argument conversions goes in opposite directions.
        M3("3", "4");   // Ambiguity, better-ness of argument conversions goes in opposite directions.
                        // Since parameter types are different ("object, string" vs. "string, object"), tie-breaking rules do not apply
    }

    static void M3(object x, params string[] y) {}
    static void M3(string x, params Span<object> y) {}
}

不過,我們的主要考慮是多載只因參數集合類型而不同,但集合類型具有相同的元素類型。 行為應該與這些案例的明確集合表達式一致。

相同的參數集對應至這兩個方法的集合元素的「」條件對於下列案例「」而言很重要:

class Program
{
    static void Main()
    {
        Test(x: 1, y: 2); // Ambiguous
    }

    static void Test(int x, params System.ReadOnlySpan<int> y) {}
    static void Test(int y, params System.Span<int> x) {}
}

「比較」由不同元素組成的集合是不合理的。

本節經過 LDM 的審查,並獲得批准。

這些規則的其中一個效果是,當暴露出不同元素類型的 params 時,使用空的參數列表呼叫它們可能會導致不明確的情況。 例如:

class Program
{
    static void Main()
    {
        // Old scenarios
        C.M1(); // Ambiguous since params arrays were introduced
        C.M1([]); // Ambiguous since params arrays were introduced

        // New scenarios
        C.M2(); // Ambiguous in C# 13
        C.M2([]); // Ambiguous in C# 13
        C.M3(); // Ambiguous in C# 13
        C.M3([]); // Ambiguous in C# 13
    }

    public static void M1(params int[] a) {
    }
    
    public static void M1(params int?[] a) {
    }
    
    public static void M2(params ReadOnlySpan<int> a) {
    }
    
    public static void M2(params Span<int?> a) {
    }
    
    public static void M3(params ReadOnlySpan<int> a) {
    }
    
    public static void M3(params ReadOnlySpan<int?> a) {
    }
}

考慮到我們優先考慮元素類型勝過其他一切,這看起來是合理的;在這種情況下,沒有什麼能告訴語言使用者是否會偏好 int? 而非 int

動態系結

目前 C# 程式執行時期繫結器不會將使用非陣列參數集合的候選項目的展開形式視為有效的候選項。

如果 primary_expression 沒有編譯時間類型 dynamic,則方法調用會經歷有限的編譯時間檢查,如 .12.6.5 動態成員調用的編譯時間檢查中所述。

如果只有單一候選者通過測試,則當符合下列所有條件時,候選者的呼叫會靜態綁定:

  • 候選專案是本機函式
  • 候選者不是通用的,其類型參數被明確指定。
  • 在編譯時期無法解析之候選專案的一般和展開形式之間沒有模棱兩可。

否則,調用表達式 會動態綁定。

如果只有單一候選人通過上述測試:

  • 如果該候選專案是本機函式,就會發生編譯時期錯誤;
  • 如果候選項僅在展開形式中適用,且使用非陣列參數集合,則會發生編譯時錯誤。

我們也應考慮恢復/修正影響本地函式的規範違反,請參閱 https://github.com/dotnet/roslyn/issues/71399

LDM 確認我們要修正此規格違反。

表達式樹

表達式樹狀結構不支援集合表達式。 同樣地,表達式樹中將不支持擴展形式的非陣列參數集合。 我們不會變更編譯程式將 Lambda 系結至表達式樹狀結構的方式,以避免使用擴充形式的非陣列參數集合來使用 API。

在非簡單案例中使用非數位集合的評估順序

本節經過 LDM 的審查,並獲得批准。 儘管陣列案例偏離其他集合,但官方語言規格不需要為陣列指定不同的規則。 偏差可以簡單地視為實作產物。 同時,我們不想變更陣列周圍的現有行為。

具名參數

在評估語法上前一個參數之後,就會建立並填入集合實例,但在評估語法上下一個參數之前。

例如:

class Program
{
    static void Main()
    {
        Test(b: GetB(), c: GetC(), a: GetA());
    }

    static void Test(int a, int b, params MyCollection c) {}

    static int GetA() => 0;
    static int GetB() => 0;
    static int GetC() => 0;
}

評估的順序如下:

  1. 呼叫 GetB
  2. MyCollection 已創建並已填入,GetC 在過程中被呼叫
  3. 呼叫 GetA
  4. 呼叫 Test

請注意,在參數陣列案例中,會在叫用目標方法之前建立陣列,然後以語彙順序評估所有自變數。

複合指派

在語彙上前一個索引被評估之後,集合實例就會被建立並填入,但在語彙上後一個索引被評估之前。 實例用來調用目標索引器的 getter 和 setter。

例如:

class Program
{
    static void Test(Program p)
    {
        p[GetA(), GetC()]++;
    }

    int this[int a, params MyCollection c] { get => 0; set {} }

    static int GetA() => 0;
    static int GetC() => 0;
}

評估的順序如下:

  1. GetA 被呼叫並快取
  2. MyCollection 會在程式中建立、填入和快取,而 GetC 則會在此過程中被呼叫。
  3. 索引器的 getter 被以索引的快取值叫用。
  4. 結果會遞增
  5. 索引設定器會使用索引的快取值和遞增運算的結果來執行設定。

具有空集合的範例:

class Program
{
    static void Test(Program p)
    {
        p[GetA()]++;
    }

    int this[int a, params MyCollection c] { get => 0; set {} }

    static int GetA() => 0;
}

評估的順序如下:

  1. GetA 被呼叫並快取
  2. 建立一個空的 MyCollection 並將其快取
  3. 索引器的 getter 被以索引的快取值叫用。
  4. 結果會遞增
  5. 索引設定器會使用索引的快取值和遞增運算的結果來執行設定。

物件初始化表達式

在語彙上前一個索引被評估之後,集合實例就會被建立並填入,但在語彙上後一個索引被評估之前。 實例會在必要時多次呼叫索引器的 getter(若需要)。

例如:

class C1
{
    public int F1;
    public int F2;
}

class Program
{
    static void Test()
    {
        _ = new Program() { [GetA(), GetC()] = { F1 = GetF1(), F2 = GetF2() } };
    }

    C1 this[int a, params MyCollection c] => new C1();

    static int GetA() => 0;
    static int GetC() => 0;
    static int GetF1() => 0;
    static int GetF2() => 0;
}

評估的順序如下:

  1. GetA 被呼叫並快取
  2. MyCollection 會在程式中建立、填入和快取,而 GetC 則會在此過程中被呼叫。
  3. 索引器的 getter 被以索引的快取值叫用。
  4. 系統會對 GetF1 進行評估並將其指派給在上一步驟中調整後的 F1C1 欄位。
  5. 索引器的 getter 被以索引的快取值叫用。
  6. 系統會對 GetF2 進行評估並將其指派給在上一步驟中調整後的 F2C1 欄位。

請注意,在 params 陣列情況下,系統會評估並快取其元素,但在每次調用索引器的 getter 時,會使用一個包含相同值的陣列新實例。 針對上述範例,評估的順序如下:

  1. GetA 被呼叫並快取
  2. GetC 被呼叫並快取
  3. 索引器的 getter 方法會被調用以使用快取 GetA,併入包含快取 GetC 的新陣列。
  4. 系統會對 GetF1 進行評估並將其指派給在上一步驟中調整後的 F1C1 欄位。
  5. 索引器的 getter 方法會被調用以使用快取 GetA,併入包含快取 GetC 的新陣列。
  6. 系統會對 GetF2 進行評估並將其指派給在上一步驟中調整後的 F2C1 欄位。

具有空集合的範例:

class C1
{
    public int F1;
    public int F2;
}

class Program
{
    static void Test()
    {
        _ = new Program() { [GetA()] = { F1 = GetF1(), F2 = GetF2() } };
    }

    C1 this[int a, params MyCollection c] => new C1();

    static int GetA() => 0;
    static int GetF1() => 0;
    static int GetF2() => 0;
}

評估的順序如下:

  1. GetA 被呼叫並快取
  2. 建立一個空的 MyCollection 並將其快取
  3. 索引器的 getter 被以索引的快取值叫用。
  4. 系統會對 GetF1 進行評估並將其指派給在上一步驟中調整後的 F1C1 欄位。
  5. 索引器的 getter 被以索引的快取值叫用。
  6. 系統會對 GetF2 進行評估並將其指派給在上一步驟中調整後的 F2C1 欄位。

參考安全

集合表示式 ref safety 區段 適用於以擴充的形式叫用 API 時進行參數集合的建構。

參數類型為 ref 結構時,會隱含 scoped 參數。 UnscopedRefAttribute 可用來覆寫該屬性。

元數據

在元數據中,我們可以像目前標記 params 陣列一樣,使用 System.ParamArrayAttribute標記非陣列 params 參數。 對於非陣列 params 參數,使用不同的屬性看起來會更安全。 例如,目前的 VB 編譯程式無法處理以 ParamArrayAttribute 裝飾的項目,不論是一般形式還是展開形式。 因此,新增「params」修飾詞可能會中斷 VB 使用者,而且很可能影響其他語言或工具的使用者。

鑒於此,非陣列 params 參數會以新的 System.Runtime.CompilerServices.ParamCollectionAttribute標記。

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
    public sealed class ParamCollectionAttribute : Attribute
    {
        public ParamCollectionAttribute() { }
    }
}

本節經過 LDM 的審查,並獲得批准。

未解決問題

堆疊分配

以下是 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md#unresolved-questions的引述:「大型集合的堆疊配置可能會導致堆疊溢位。」 編譯器是否應該具有啟發式演算法,以便將此數據放在堆記憶體上? 是否應該未指定語言以允許此彈性? 我們應該遵循 params Span<T>的規格。這聽起來像是我們必須在這個提案的內容中回答問題。

[已解決]隱含 scoped 參數

有人建議,當 params 修改 ref struct 參數時,應該視為宣告 scoped。 自變數是在查看 BCL 案例時,您想要設定參數範圍的案例數目幾乎為 100%。 在少數需要的情況下,可能會以 [UnscopedRef]覆寫預設值。

不過,僅根據 params 修飾詞的存在來更改預設值可能是不理想的。 特別是,在覆寫/實作案例中,params 修飾詞不一定相符。

解析度:

參數具有隱式作用域 - https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-11-15.md#params-improvements

[已解決]請考慮在覆寫中施行 scopedparams

我們先前已指出 params 參數預設應 scoped。 不過,這會在覆寫時引入奇怪的行為,因為我們現有的重述 params規則:

class Base
{
    internal virtual Span<int> M1(scoped Span<int> s1, params Span<int> s2) => throw null!;
}

class Derived : Base
{
    internal override Span<int> M1(Span<int> s1, // Error, missing `scoped` on override
                                   Span<int> s2  // Proposal: Error: parameter must include either `params` or `scoped`
                                  ) => throw null!;
}

我們在此對於攜帶 params 和運用 scoped 在覆寫中的行為存在差異:params 是隱含繼承的,並且 scoped也會隨之繼承,而 scoped 本身 則不會隱含繼承,需要在每個層級重複。

提案:如果原始定義是 params 參數,我們應該強制要求覆寫 params 參數時必須明確陳述 scopedscoped。 換句話說,s2 中的 Derived 必須有 paramsscoped或兩者。

解析度:

若需使用非scoped 參數覆寫時,我們必須明確指定 paramsparams 在覆寫 params 參數時的使用方式 - https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-21.md#params-and-scoped-across-overrides

[已解決]必須的成員是否應該阻止宣告 params 參數?

請考慮下列範例:

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

public class MyCollection1 : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
    public void Add(long l) => throw null;

    public required int F; // Collection has required member and constructor doesn't initialize it explicitly
}

class Program
{
    static void Main()
    {
        Test(2, 3); // error CS9035: Required member 'MyCollection1.F' must be set in the object initializer or attribute constructor.
    }

    // Proposal: An error is reported for the parameter indicating that the constructor that is required
    // to be available doesn't initialize required members. In other words, one is able
    // to declare such a parameter under the specified conditions.
    static void Test(params MyCollection1 a)
    {
    }
}

解析度:

我們將針對用來判斷是否符合資格成為 required 參數的建構函式,在宣告位置驗證 params 成員 - https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-21.md#required-members-and-params-parameters

替代方案

有一個 替代提案,僅針對 params延長 ReadOnlySpan<T>

此外,人們可能會說,現在語言中有了 集合表達式,完全沒有必要擴充 params 的支援。 針對任何集合類型。 若要使用集合類型的 API,開發人員只需在展開的引數清單前加上兩個字元 [,並在清單後加上 ]。 鑒於此情況,擴充 params 支援可能是過分,尤其是其他語言不太可能支援使用非陣列 params 參數。