1. 程式人生 > 其它 >關於 Span 的一切:探索新的 .NET 明星: 1 Span<T> 是什麼?

關於 Span 的一切:探索新的 .NET 明星: 1 Span<T> 是什麼?

關於 Span 的一切:探索新的 .NET 明星

https://docs.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay

想象一下你正在釋出一個特別的排序演算法程式,它可以在記憶體中就地處理資料。你會希望釋出一個獲得一個數組引數,並提供在陣列之上操作 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 索引器相比,它不是 ref returning 的。下面是一個示例:

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 型別。