C# 的集合運算式(Collection Expressions)

摘錄 《現代 C#:AI 時代的開發者修煉》 第 2 章有關集合運算式(Collection Expressions)的介紹。




C# 12 引入了集合運算式(Collection Expressions)之後,方括弧 [] 幾乎成了處理集合的首選語法。它不只是把程式碼寫得更短,更重要的是把陣列、List<T>Span<T>ReadOnlySpan<T> 這些常見集合的初始化方式統一起來,也讓編譯器更有空間做最佳化。


先抓住一個核心觀念:右邊的 [...] 只是元素序列的寫法;真正決定結果型別的,是左邊或使用情境提供的目標型別(target type)。

使用時機

本節整理集合運算式的幾個常見的使用情境,包括:

  • 初始化集合時
  • 合併多個集合時
  • 傳遞參數給方法時
  • 需要空集合時
  • 寫 Span<T> 或 ReadOnlySpan<T> 的程式時

初始化集合

這是最直觀、也最常見的使用時機。過去初始化不同集合型別時,語法風格差很多;集合運算式把它們統一成同一種寫法。

以前:

int[] numbers = new int[] { 1, 2, 3 };
List<string> strings = new List<string> { "A", "B", "C" };

現在可以這樣寫:

int[] numbers = [1, 2, 3];
List<string> strings = ["A", "B", "C"];

你可以看到,使用集合運算式的時候,程式碼右邊的寫法相當一致。編譯器會依據左邊的目標型別決定要建立什麼集合。這也是集合運算式最大的價值之一:語法一致,但結果型別仍然保有型別安全與語意清楚的特性。

合併多個集合

如果你需要把多個集合拼成一個新的集合,集合運算式搭配 .. 展開(spread)語法很好用。它通常比 AddRangeConcat(...).ToArray() 或手動複製更直觀。

int[] section1 = [1, 2];
int[] section2 = [5, 6];

int[] allLevels = [0, .. section1, 3, 4, .. section2, 7];
// 結果: [0, 1, 2, 3, 4, 5, 6, 7]

這裡的 .. section1 表示把 section1 內的元素逐一展開後放進新的集合運算式中。

Note

這裡的 .. 是集合運算式中的展開語法;它和第 6 章清單模式(list pattern)裡的 .. 長得一樣,但用途不同。前者是建立新集合時展開元素,後者是模式比對時匹配一段元素


傳遞參數給方法

集合運算式不只出現在變數宣告,也很適合直接出現在方法呼叫處。只要方法參數型別明確,呼叫端通常就可以直接用 []

void PrintTags(ReadOnlySpan<string> tags)
{
    foreach (var tag in tags)
        Console.WriteLine(tag);
}

PrintTags(["C#", ".NET", "Coding"]);

這種寫法的好處是:你在呼叫端只描述「我要哪些元素」,而不必被迫暴露中間集合的建構細節。

此外,從 C# 13/.NET 9 開始,params 參數不再侷限於陣列,而可以使用更多種集合,例如 IList<T>Span<T>ReadOnlySpan<T> 等等,所以集合運算式也可以搭配 params 集合參數,讓呼叫端的寫法更彈性、多樣:

void PrintNumbers(params IList<int> numbers) // 需要 C# 13 / .NET 9+
{
    foreach (var n in numbers)
        Console.WriteLine(n);
}

// 呼叫端用以下三種寫法都可以
PrintNumbers(1, 2, 3);
PrintNumbers([1, 2, 3]);
PrintNumbers(new List<int> { 1, 2, 3 });

需要空集合時

空集合也是集合運算式很適合發揮的地方。和 new string[0]new List<int>() 這些舊寫法相比,[] 更直接,也更容易閱讀。

int[] emptyArray = [];
List<string> tags = [];
ReadOnlySpan<int> values = [];

當目標型別明確時,編譯器也可能為空集合選擇更有效率的實作方式;例如在適合的情況下,陣列型別可能會使用類似 Array.Empty<T>() 的共享空陣列。

寫 Span<T> 或 ReadOnlySpan<T> 的程式時

集合運算式在 Span<T> 與 ReadOnlySpan<T> 的場景尤其值得注意。這類型別常見於剖析器(parser)、語彙分析器(tokenizer)、資料處理或其他重視效能的程式碼,而集合運算式讓它們的初始化寫法比 stackalloc 更一致、也更容易讀。

範例:

ReadOnlySpan<byte> header = [0xDE, 0xAD, 0xBE, 0xEF];

要提醒的是,當目標型別是 Span<T> 或 ReadOnlySpan<T> 時,編譯器在安全可行的前提下,可能採用堆疊配置,但它不保證一定等同於你手寫的 stackalloc。針對此議題,本書第 12 章還會再從高效能記憶體操作的角度進一步說明。

原始碼: DemoCollectionExpressions

兩個常見限制

雖然集合運算式很強大,但有兩個小限制,一個是無法推斷型別,另一個則和 Dictionary 有關。以下分別說明。

不能靠 var 自行推斷型別

下面這種寫法不能通過編譯:

var numbers = [1, 2, 3];  // ✗ 編譯失敗

原因是編譯器不知道你要的是 int[]List<int>Span<int>,還是其他受支援的集合型別。你必須把目標型別寫出來:

int[] numbers = [1, 2, 3];     // OK
List<int> values = [1, 2, 3];  // OK

這也再次說明了集合運算式是 target-typed 語法,而不是擁有固定自然型別的 literal。

Dictionary 初始化的限制

目前集合運算式主要針對「單一元素型別的序列」設計;對 Dictionary<TKey, TValue> 這種需要鍵值配對語意的型別,並沒有對應的鍵值項目語法。因此,如果你要建立帶初始內容的字典,通常仍要使用傳統的初始化寫法,例如:

var scores = new Dictionary<string, int>
{
    ["Alice"] = 95,
    ["Bob"] = 87
};

不過若你只是要表示空字典,則仍可寫成:

Dictionary<string, int> emptyScores = [];


本文摘自 《現代 C#:AI 時代的開發者修煉》 第 2 章,有針對部落格排版稍微調整。


沒有留言:

技術提供:Blogger.
回頂端⬆️