本節補充 .NET 中三個彼此互補的概念:不可變的 System.String
、輕量且高效的 Span<T>
(ref struct)與可跨執行期間傳遞的 Memory<T>
。說明它們的記憶體語意、常見使用情境、效能考量與實作範例。
Memory<T>
,短暫、同步、低延遲操作使用 Span<T>
。Replace
、Substring
、插接等)。AsSpan()
取得 ReadOnlySpan<char>
,用於避免產生中間字串的讀取操作。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>
來避免分配。
ref struct
,表示連續記憶體的安全視圖(view),可以包裝陣列、stackalloc、unmanaged memory 或 Memory<T>.Span
。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>
。
ref struct
,可以放在堆上、做為欄位、並且可以安全地跨 async/await 邊界傳遞。Span<T>
的關係:Memory<T>
可透過 .Span
取得 Span<T>
;反之則不能(Span 無法被封箱為 Memory)。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。