1. 介紹
當你處理陣列、字串和位元組緩衝區時,經常會碰到想「看」資料某一部分的需求。例如,擷取子字串、取陣列的切片、處理輸入串流的一段。在舊版的 .NET 裡,通常要麼複製資料(建立新的陣列/子字串),要麼寫迴圈從某個索引跑到結尾。這些方法在效能和可讀性上都不太理想。
以下是一個舊做法的範例:我們只想把大陣列的一部分傳給某個方法:
// 舊做法 — 複製陣列的一部分(效率差!)
int[] source = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
int[] subArray = source.Skip(2).Take(4).ToArray(); // 會建立一個新陣列
因此,若要有效率地傳遞陣列的「切片」(甚至是字串的一段)而不建立不必要的物件,舊的 C# 工具就顯得不足,特別是面對大量資料時效果尤其差。
這時候,主角登場 — Span<T>!
2. 什麼是 Span<T>? 基本概念
Span<T> 是一個型別,表示同一型別 T 的連續記憶體區域。它的任務是提供一個快速、安全且有效率的方式,來操作陣列、字串、結構,以及非受控記憶體(例如 .NET 管理環境之外分配的記憶體)。
Span<T> 的關鍵特性是「切片而不建立新陣列」。想像一把尺,你可以用它測量同一個陣列的任意區段,而不複製資料,並把錯誤索引的風險降到最低。
簡短說明:
- Span<T> — 是一個針對記憶體片段的「視窗」或「檢視」,可以方便且安全地操作該片段。
- 不會分配新的記憶體 — 節省資源,減少 GC 的工作。
- 不只用於陣列,也可以用在字串的片段、stackalloc 區塊,甚至非受控記憶體。
- 不能存放在一般類別的欄位中:它是堆疊專用型別(stack-only struct)。
為什麼這很重要?
在高效能場景(檔案解析、大型緩衝區處理、加密、序列化)中,哪怕省下幾次陣列複製都能大幅提速並降低垃圾回收(GC)壓力。還有,這也顯示你對現代 C# 與 .NET 的掌握。
3. Span<T> 的基本使用:第一個切片
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 在陣列上建立 Span 的一部分(例如,從第 2 到第 5 個元素,包含端點)
Span<int> middle = new Span<int>(numbers, 2, 4); // 索引: 2, 3, 4, 5
// 我們看到的子陣列: 3, 4, 5, 6
Console.WriteLine(string.Join(", ", middle.ToArray())); // 3, 4, 5, 6
// 修改 Span 會改變原始陣列!
middle[1] = 999;
Console.WriteLine(numbers[3]); // 999
注意! Span<T> 不會複製資料,它只是指向陣列的「片段」。所有變更在原陣列與 Span 中都會反映出來。
4. 建立 Span<T> 的主要方式
基於陣列:
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> span = arr; // 整個長度
Span<int> slice = arr.AsSpan(1, 3); // 元素 20, 30, 40
基於陣列的一部分:
Span<int> part = new Span<int>(arr, 2, 2); // 元素 30, 40
stackalloc:在堆疊上分配記憶體(非常快且不會進入 heap):
Span<byte> buffer = stackalloc byte[128];
buffer[0] = 42;
使用 .Slice() 方法:
Span<int> subSpan = span.Slice(1, 2); // 元素 20, 30
切片的視覺化示意
原始陣列: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
<--- Span: 3, 4, 5, 6 --->
5. Span<T> 的限制與特性
- 僅限堆疊(Stack-only)! 不能當作一般類別的欄位,也不能成為閉包的一部分 — 它是堆疊型別。
- 不能作為類別欄位或從 async 方法返回(編譯器會報錯)。
- 不能在 lambda/匿名方法中被捕獲 — 要「當下使用」。
- 不能直接序列化或跨執行緒傳遞。
這是因為 Span 可能指向任意記憶體區域,如果它被移到 heap,就可能導致不安全的狀態。
6. 不可變版本:ReadOnlySpan<T>
有時你只想「看」記憶體的一部分,但不想修改它。這種情況下可以使用不可變的版本 — ReadOnlySpan<T>。
string text = "Hello, Span!";
ReadOnlySpan<char> letters = text.AsSpan(7, 4); // 'S', 'p', 'a', 'n'
Console.WriteLine(string.Join(", ", letters.ToArray())); // S, p, a, n
// letters[0] = 'Z'; // 錯誤: 索引子是唯讀的!
典型使用情境是安全地傳遞字串或陣列的「片段」,到某處不應該(也不會)被修改的地方。
7. 範例實作:從字串與陣列切片
假設這是一個資料解析器,會從一個很長的字串中擷取子字串、尋找其中的數字並回傳它們的總和(在初始切片時不做多餘複製):
using System;
class Program
{
static void Main()
{
// 假設使用者輸入了一串由空格分隔的長數字字串
string input = "12 34 56 78 90 123 456 789";
// 我們只想計算中間的數字總和,例如: 56 78 90
// 取出子字串(但不複製!)
ReadOnlySpan<char> center = input.AsSpan(6, 8); // 索引可以動態計算
// 用 Split 解析數字(會建立暫時的陣列)
string[] numbers = center.ToString().Split(' ');
int sum = 0;
foreach (var str in numbers)
{
if (int.TryParse(str, out int num))
sum += num;
}
Console.WriteLine($"中央數字的總和: {sum}");
}
}
現代的 CSV/JSON 解析函式庫會使用 Span 來加速大量字串資料的處理——現在你知道他們的「秘訣」了。
8. 有用的細節
Span vs 陣列複製
// 舊方法: 複製陣列的一部分
int[] arr = Enumerable.Range(0, 1000000).ToArray();
int[] firstThousand = arr.Take(1000).ToArray(); // 建立一個新的 1000 元素陣列
// 新方法: 使用 Span
Span<int> bestThousand = arr.AsSpan(0, 1000); // 完全不複製!
bestThousand[0] = 42; // arr 也會改變
在大量解析檔案、處理網路緩衝或二進位資料時,差別特別明顯。
真實場景應用:為什麼要知道 Span
- 高效能的解析與處理字串/二進位資料。現代序列化函式庫(例如 System.Text.Json、Span 的 Microsoft 文件)使用 Span 來加速處理。
- 緩衝處理與檔案讀取(在不複製的情況下切割大型緩衝)。
- 在記憶體受限的設備(embedded、IoT)中處理資料時非常有用 — 參考 Memory/Spans 的官方文件。
- 影像與音訊的演算法,對速度與避免額外分配有高度依賴。
- 使用 Span 加速 CSV、JSON、XML 的解析 — 在 .NET 8/9 中尤其常見。
面試中關於 Span 的問題自從它在 .NET Core 2.1 引入後就開始出現,在 .NET 9 的環境下越來越被期待熟悉。
示意圖:哪裡是 Span,哪裡是陣列
+--------------------+
| int[] 陣列 |
| 1 2 3 4 5 6 7 8 |
+--------------------+
^ ^
| |
[ 2, 3, 4, 5 ] <-- Span<int> 的「記憶體視窗」(slice)
Span<T> 不是一個獨立陣列,而是對資料的一個「透明鏡片」的檢視。
與其他集合的差異:比較表
| 型別 | 儲存資料? | 能否修改元素? | 能否改變大小? | 切片時是否複製? | 在哪裡存在? |
|---|---|---|---|---|---|
|
會 | 可以 | 不行 | 會(透過 .Take) | Heap |
|
會 | 可以 | 可以 | 會 | Heap |
|
不會 | 可以 | 不行 | 不會 | Stack |
|
不會 | 不可以 | 不行 | 不會 | Stack |
9. 使用 Span/ReadOnlySpan 常見錯誤
錯誤 #1:嘗試把 Span 當成類別的欄位存起來。編譯器會報錯 “Span type may not be used in this context”。這是故意設計:把 Span 存在欄位會不安全。
錯誤 #2:從 async 方法返回 Span。不能這麼做,因為 async 方法可能會把執行搬到 heap。改用陣列或其他類型。
錯誤 #3:忘了經由 Span 所做的修改會反映到原始陣列。這可能會在外部導致意外的資料變動,造成不可預期的行為。
GO TO FULL VERSION