關於 Span 的一切:探索新的 .NET 明星: 1 Span<T> 是什麼?
關於 Span 的一切:探索新的 .NET 明星
想象一下你正在釋出一個特別的排序演算法程式,它可以在記憶體中就地處理資料。你會希望釋出一個獲得一個數組引數,並提供在陣列之上操作 T[] 的實現。如果呼叫方可以獲得這個陣列,並且是希望對整個陣列進行排序,那麼這種方式特別棒。但是,如果呼叫方僅僅希望對陣列的部分進行排序呢,你可能又會出提供一個過載的實現,通過 offset 和 count 引數來支援。不過,如果你又希望支援記憶體中不是陣列的資料,比如說,而是來自原生程式碼呢?或者是在堆疊上的資料,你只有指向它的指標和長度呢?你又如何開發你的排序方法來操作此類任意的記憶體區域,而且仍然與處理整個陣列,或者陣列的子集一樣好呢?還要考慮到處理託管陣列與非託管陣列一樣好呢?
或者,我們看另一個例子。你正在實現一個對 System.String 的處理。例如是一個特別的解析方法。你希望獲得一個字串引數,然後提供提供處理字串的實現。但是,如果你希望還要支援處理該字串的子集呢?String.Substring() 方法可以用來抽取出感興趣的一部分字串,但是這會牽涉到昂貴的操作,導致字串分配和記憶體複製。你也可以這樣做,如在陣列示例中那樣,通過一個 offset 和 count 引數來處理。不過,如果呼叫方並沒有得到這個字串,而是得到了一個 char[] 陣列呢?或者呼叫方得到的是指標 char* 呢?或者是通過呼叫 stackalloc 使用棧空間呢?或者是呼叫原生程式碼得到的結果呢?你又如何開發你的解析方法,不需要強制呼叫者做任何記憶體分配或者複製的一種方式呢?並且仍然一致良好地處理各種輸入型別,比如字串、char[] 和 char* 呢?
在這兩種場景下,你可能可以使用 unsafe 程式碼和指標來完成,提供接受指標和長度的輸入。不過,這樣丟掉了 .NET 的核心的安全保證,打開了問題之門,比如緩衝區溢位,訪問衝突等等,這些對大多數 .NET 開發者已經過去的問題。它還引入了額外的效能懲罰,比如需要在處理過程中釘住託管物件,以便你獲得的指標保持有效。基於底層不同的資料型別,並不總是可以獲得指標。
對於這個謎題的答案就是,Span <T>
什麼是 Span<T>?
System.Span<T> 是來自 .NET 核心的新的值型別。它支援表示記憶體中任意一段連續的區間,不管這段記憶體屬於一個託管物件,還是通過原生程式碼進行互操作得到,或者是被分配在堆疊上。儘管這樣還提供了類似陣列操作的高效能的安全訪問。
例如,你可以從陣列來建立 Span<T>:
var arr = new byte[10];
Span<byte> bytes = arr; // Implicit cast from T[] to Span<T>
從這裡開始,你可以簡單且高效地利用 Span 的 Slice() 過載方法,建立一個 Span 來表示/指向該陣列的一個子集。通過它,你可以通過下標來讀、寫源陣列相關的部分。
Span<byte> slicedBytes = bytes.Slice(start: 5, length: 2);
slicedBytes[0] = 42;
slicedBytes[1] = 43;
Assert.Equal(42, slicedBytes[0]);
Assert.Equal(43, slicedBytes[1]);
Assert.Equal(arr[5], slicedBytes[0]);
Assert.Equal(arr[6], slicedBytes[1]);
slicedBytes[2] = 44; // Throws IndexOutOfRangeException
bytes[2] = 45; // OK
Assert.Equal(arr[2], bytes[2]);
Assert.Equal(45, arr[2]);
如前所述,Span 還提供了訪問陣列子集的一種方式。它也可以用來指向堆疊上的資料。例如:
Span<byte> bytes = stackalloc byte[2]; // Using C# 7.2 stackalloc support for spans
bytes[0] = 42;
bytes[1] = 43;
Assert.Equal(42, bytes[0]);
Assert.Equal(43, bytes[1]);
bytes[2] = 44; // throws IndexOutOfRangeException
更為方便的是,它可以用來指向任意的指標和長度,例如通過本地堆分配的記憶體,例如:
IntPtr ptr = Marshal.AllocHGlobal(1);
try
{
Span<byte> bytes;
unsafe { bytes = new Span<byte>((byte*)ptr, 1); }
bytes[0] = 42;
Assert.Equal(42, bytes[0]);
Assert.Equal(Marshal.ReadByte(ptr), bytes[0]);
bytes[1] = 43; // Throws IndexOutOfRangeException
}
finally { Marshal.FreeHGlobal(ptr); }
Span<T> 的索引器藉助於被稱為 ref return 的從 C# 7.0 引入的 C# 特性。該索引器使用 ref T
返回型別定義。它提供了類似於索引陣列的語法,返回實際儲存位置的引用,而不是在該位置記憶體的複製品。
public ref T this[int index] { get { ... } }
通過該示例,該 ref-returning 索引器的影響顯而易見,例如與 List
struct MutableStruct { public int Value; }
...
Span<MutableStruct> spanOfStructs = new MutableStruct[1];
spanOfStructs[0].Value = 42;
Assert.Equal(42, spanOfStructs[0].Value);
var listOfStructs = new List<MutableStruct> { new MutableStruct() };
listOfStructs[0].Value = 42; // Error CS1612: the return value is not a variable
Span<T> 的一種變體,被稱為 System.ReadOnlySpan<T>,支援只讀訪問。該型別與 Span<T> 類似,除了藉助於 C#7.2 中引入的 ref readonly T
特性,而不是 ref T
。使得它可以處理不變的資料型別,比如 System.String。ReadOnlySpan<T> 使得可以非常高效地處理字串切片,而不需要分配或者複製記憶體,例如:
string str = "hello, world";
string worldString = str.Substring(startIndex: 7, length: 5); // Allocates
ReadOnlySpan<char> worldSpan =
str.AsSpan().Slice(start: 7, length: 5); // No allocation
Assert.Equal('w', worldSpan[0]);
worldSpan[0] = 'a'; // Error CS0200: indexer cannot be assigned to
除了這些已經介紹的特性,Span 還提供了多種優點。例如,Span 支援型別轉換符號,意味著你可以強制一個 Span<byte> 到 Span<int> ( 這裡 Span<int> 的 0 下標對映到 Span<byte> 第一個 4 位元組 )。這樣如果你讀取 bytes 緩衝區,你可以安全且高效地將它傳遞給操作一組位元組,例如 int 型別。