CodeGym /課程 /C# SELF /介紹 Span<T> 和 ReadOnlySpan<T>

介紹 Span<T> 和 ReadOnlySpan<T>

C# SELF
等級 65, 課堂 2
開放

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> 不是一個獨立陣列,而是對資料的一個「透明鏡片」的檢視。

與其他集合的差異:比較表

型別 儲存資料? 能否修改元素? 能否改變大小? 切片時是否複製? 在哪裡存在?
int[]
會 可以 不行 會(透過 .Take) Heap
List<int>
會 可以 可以 會 Heap
Span<int>
不會 可以 不行 不會 Stack
ReadOnlySpan<int>
不會 不可以 不行 不會 Stack

9. 使用 Span/ReadOnlySpan 常見錯誤

錯誤 #1:嘗試把 Span 當成類別的欄位存起來。編譯器會報錯 “Span type may not be used in this context”。這是故意設計:把 Span 存在欄位會不安全。

錯誤 #2:從 async 方法返回 Span。不能這麼做,因為 async 方法可能會把執行搬到 heap。改用陣列或其他類型。

錯誤 #3:忘了經由 Span 所做的修改會反映到原始陣列。這可能會在外部導致意外的資料變動,造成不可預期的行為。

2
任務
C# SELF, 等級 65, 課堂 2
上鎖
透過 `Span` 進行修改
透過 `Span` 進行修改
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION