CodeGym /課程 /C# SELF /新的功能 Span<T>

新的功能 Span<T>

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

1. 介紹

想像你有一個很大的資料陣列,例如從檔案讀取的位元組。你想處理它的一個片段——把一部分當成字串讀取,或只是把一段資料傳給方法。在「舊式」做法中,你會把那些位元組複製到新的陣列——這既慢又耗記憶體。如果有好幾十或好幾百個這樣的片段呢?

這時候主角出場 —— Span<T>。這是一個特殊的結構,用來描述陣列(或任何連續記憶體片段)上的切片(view),不複製資料,只是指向所需的區域。重要:Span<T> 不是新的集合,而是現有陣列的一個安全「視窗」!

Span<T> 的主要特性與限制

  • Span<T> 是一個結構(value type),表示記憶體上的「視窗」,不會複製元素。
  • 只參考連續的記憶體區段:陣列、陣列的一部分、stackalloc 區塊、透過特別方法的字串記憶體,或 unsafe 代碼中的記憶體。
  • Span<T> 存活在堆疊上。不能把它存放在類別欄位,也不能直接從 async 方法回傳。
  • 保證型別安全:存取記憶體不需要手動使用指標(除非你使用 unsafe)。

簡短介紹 C# 14 的新功能

C# 14 擴展了對切片的支援:更方便的範圍運算子 .. 和 從尾部索引 ^1,針對陣列和切片改進的匹配(pattern);以及其他語法糖。稍後會回到這部分。

2. 如何建立 Span<T>?

從簡單的陣列開始,建立一個 span:

int[] numbers = { 10, 20, 30, 40, 50, 60 };
Span<int> midSpan = new Span<int>(numbers, 2, 3); // 從第 3 個元素 (索引 2) 取 3 個元素

現在 midSpan 指向元素 30、40、50 — 這不是複本,而是對陣列「實時」元素的視圖。透過 span 修改它們會改變原來的陣列。

midSpan[0] = 100;
Console.WriteLine(numbers[2]); // 會輸出 100

為什麼不直接用陣列切片?

經典的 LINQ 切片會建立一個 新的陣列:

int[] slice = numbers.Skip(2).Take(3).ToArray(); // <-- 這裡會建立複本!

這在時間和記憶體上都很昂貴。Span 解決了不必要複製的問題——而且不只適用於數字。

3. 更多建立 Span<T> 的方式

現有的陣列

Span<int> mySpan = numbers; // 從陣列到 Span 的隱式轉換

陣列的一部分

int[] numbers = {10, 20, 30, 40, 50, 60};
Span<int> part = numbers.AsSpan(1, 4); // 從索引 1 開始的 4 個元素: {20, 30, 40, 50}

堆疊記憶體 (stackalloc)

Span<T> 允許在堆疊上配置陣列:

Span<byte> buffer = stackalloc byte[128];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = (byte)i;

堆疊記憶體很快,並且在方法結束時自動釋放。但大小應該合理——堆疊上放幾兆位元組是不被允許的。

字串與 ReadOnlySpan<char>

.NET 中的字串是不可變的,所以我們用 ReadOnlySpan<char>:

string greeting = "Hello, C# world!";
ReadOnlySpan<char> span = greeting.AsSpan(7, 8);   // "C# world"
Console.WriteLine(span.ToString());                // C# world

4. 來組合一個範例!

using System;

class Program
{
    static void Main()
    {
        int[] orderTotals = { 100, 200, 300, 400, 500, 600, 700 };
        Console.WriteLine("整個訂單歷史: ");
        foreach (int total in orderTotals)
            Console.Write(total + " ");
        Console.WriteLine();
        
        Console.WriteLine("輸出第 2 到第 4 筆訂單 (索引 1-3):");
        Span<int> recentOrders = orderTotals.AsSpan(1, 3);
        foreach (int t in recentOrders)
            Console.Write(t + " ");
        Console.WriteLine();
        
        // 透過 Span 變更資料
        recentOrders[1] = 999;
        Console.WriteLine("透過 Span 修改後:");
        foreach (int total in orderTotals)
            Console.Write(total + " ");
        Console.WriteLine();
    }
}

切片 recentOrders 真正地直接作用在陣列上——它不是複本。

5. 安全性、效能與檢查

  • 節省記憶體:沒有不必要的複本。
  • 邊界檢查保護你不會越界存取陣列。
  • JIT 的優化能帶來非常快速的存取(在不使用 unsafe 的情況下)。

與方法的互動

方法可以接受 Span<T> 或 ReadOnlySpan<T>。如果不打算修改資料——就用 ReadOnlySpan<T>。

static int Sum(Span<int> slice)
{
    int sum = 0;
    foreach (var item in slice)
        sum += item;
    return sum;
}

int[] data = { 1, 2, 3, 4, 5, 6, 7 };
Console.WriteLine(Sum(data.AsSpan(2, 3))); // 3+4+5=12

6. 使用範圍 (range)

從 C# 8 起,可使用範圍運算子 .. 和 從尾部索引 ^ —— 它們與切片搭配得很好。

int[] arr = { 10, 20, 30, 40, 50, 60 };

Span<int> span = arr[2..5]; // 索引 2,3,4 - 也就是 30, 40, 50

在範圍中,起始索引包含,結束索引不包含。

從尾部索引

int lastElement = arr[^1];     // 最後一個元素 (60)
Span<int> lastTwo = arr[^2..]; // 兩個最後的元素 (50, 60)

範圍與字串

string code = "SpanMagic!";
var mid = code[4..9]; // "Magic"

這個切片是 ReadOnlySpan<char>;如果要取得字串,呼叫 ToString()。

7. 現代的切片處理模式 (C# 14)

新的模式讓解析陣列與切片更簡單。

if (arr is [10, 20, .. var rest]) // .. 捕獲陣列的「尾端」
{
    Console.WriteLine("開頭匹配,尾端:");
    foreach (var x in rest)
        Console.WriteLine(x);
}
if (arr is [.., 50, 60])
    Console.WriteLine("陣列以 50, 60 結尾");

比較:陣列、ArraySegment 和 Span

類型 可變更 會複製資料? 可在堆疊上 類別/結構 可做為類別欄位
T[]
是 - 否 class 是
ArraySegment<T>
是 否 否 struct 是
Span<T>
是 否 是 struct 否
ReadOnlySpan<T>
否 否 是 struct 否

換句話說,Span<T> 是 ArraySegment<T> 的演進:更效能導向且更安全。

8. 常見錯誤與坑

嘗試把 Span<T> 存在類別欄位。 不行:欄位存在於 heap,而 Span<T> 應該存在於 stack。請使用 ArraySegment<T> 或用索引來定位。

從 async 方法回傳/儲存 Span<T>。 不行:非同步會斷開堆疊。改用其他方式傳遞資料,例如陣列、Memory<T>/ReadOnlyMemory<T>。

建立切片時使用錯誤的範圍。 越界會在執行時拋出例外。建立切片前務必檢查長度與邊界。

忘記 span 會反映原始資料。 透過 Span<T> 的任何修改都會改變原來的陣列。如果需要獨立的複本——請顯式複製資料。

2
任務
C# SELF, 等級 65, 課堂 3
上鎖
使用範圍 (Range) 和從結尾開始的索引來顯示部分陣列
使用範圍 (Range) 和從結尾開始的索引來顯示部分陣列
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION