iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0

BCL 字串、Span、Memory

本節補充 .NET 中三個彼此互補的概念:不可變的 System.String、輕量且高效的 Span<T>(ref struct)與可跨執行期間傳遞的 Memory<T>。說明它們的記憶體語意、常見使用情境、效能考量與實作範例。

  • 輸入:字元與位元組來源 string、byte[]、ArraySegment、stream、stackalloc buffer、native pointer
  • 輸出:高效讀寫,不必要的複製最小化;在需要跨 async/await 或當作欄位時使用 Memory<T>,短暫、同步、低延遲操作使用 Span<T>
  • 成功準則:不會產生不必要 GC allocation、避免無效的記憶體存取、在安全限制下取得高效能。

System.String(BCL 字串)

  • 本質:不可變(immutable)、以 UTF-16 編碼儲存在託管堆上的參考型別(reference type)。
  • 特色:
    • 一旦建立內容不可改變,所有修改操作會產生新字串(例如 ReplaceSubstring、插接等)。
    • 字串字面量會被駐留(interned),相同字面量會共用相同物件。
    • 支援 AsSpan() 取得 ReadOnlySpan<char>,用於避免產生中間字串的讀取操作。
  • 常見 API:string.AsSpan(), string.Substring(), string.Create(...)
using System;

ReadOnlySpan<byte> bytes = new byte[] { 0xE4, 0xBD, 0xA0 }; // UTF-8 範例(部分)
// 使用 Encoding 的 ReadOnlySpan overload 來避免中間陣列
string s = System.Text.Encoding.UTF8.GetString(bytes);
Console.WriteLine(s);
// 如果已經有 string,可以立刻取得 ReadOnlySpan<char>
string hello = "Hello World";
ReadOnlySpan<char> span = hello.AsSpan(6, 5); // "World"

注意:Substring 會配置新字串;若要做大量切片、解析或掃描,優先使用 Span<char> / ReadOnlySpan<char> 來避免分配。

Span

  • 本質:一個 ref struct,表示連續記憶體的安全視圖(view),可以包裝陣列、stackalloc、unmanaged memory 或 Memory<T>.Span
  • 重要屬性與限制:
    • 是 stack-only(不能放到托管堆上),因此不能作為欄位、不能被裝箱、不能傳回做為長期儲存,也不能在 async 方法中被攜帶(例如 await 會捕獲上下文)。
    • 切片(Slice)與索引是 O(1) 並且不分配。
    • 非常適合短生命週期、同步、效能敏感的操作(文字解析、序列化、快取緩衝)。
using System;

void ParseKeyValue(ReadOnlySpan<char> input)
{
        // 假設格式 "key:value"
        int idx = input.IndexOf(':');
        if (idx >= 0)
        {
                var key = input.Slice(0, idx);
                var value = input.Slice(idx + 1);
                // 使用 key / value,但不會產生新的 string
                Console.WriteLine($"Key: {key.ToString()}, Value: {value.ToString()}");
        }
}

ParseKeyValue("name:alice".AsSpan());

使用 stackalloc 在 stack 上配置暫時緩衝

Span<byte> buffer = stackalloc byte[256];
int read = GetData(buffer); // 假想的同步 API
// 處理 buffer.Slice(0, read)

int GetData(Span<byte> b)
{
        // 模擬填充
        var data = new byte[] {1,2,3,4};
        data.AsSpan().CopyTo(b);
        return data.Length;
}

ref struct 的限制是刻意設計來避免發生隱含的記憶體壽命問題。若需要跨越 async/await 或當作欄位,改用 Memory<T>


Memory 與 ReadOnlyMemory

  • 本質:以值型態封裝一段連續記憶體,但不是 ref struct,可以放在堆上、做為欄位、並且可以安全地跨 async/await 邊界傳遞。
  • Span<T> 的關係:Memory<T> 可透過 .Span 取得 Span<T>;反之則不能(Span 無法被封箱為 Memory)。
  • 典型使用場景:
    • 非同步 API(例如從 socket / stream 非同步讀取資料),需要將緩衝區保留在堆上直到 I/O 完成
    • 作為資料所有權(ownership)傳遞,例如使用 MemoryPool<T>.Rent()IMemoryOwner<T>
using System;
using System.Buffers;
using System.Threading.Tasks;

async Task ReadFromStreamAsync(System.IO.Stream stream)
{
        using var owner = MemoryPool<byte>.Shared.Rent(4096); // IMemoryOwner<byte>
        Memory<byte> memory = owner.Memory;
        int read = await stream.ReadAsync(memory);
        // 轉成 Span 進行同步處理
        var span = memory.Span.Slice(0, read);
        Process(span);
}

void Process(Span<byte> s)
{
        // 處理二進位資料
}

Memory<T> 本身不會 pin GC 物件;若需要傳給 native API,必須在固定期間使用 MemoryMarshal.GetReference(memory.Span) 並在 fixed 區塊中使用或使用 GCHandle pin。


上一篇
PGO 與性能
下一篇
Span / Memory 互轉技巧
系列文
新 .NET & Azure & IoT & AI 開源技術實戰手冊 (含深入官方程式碼講解) 22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言