日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > C# >内容正文

C#

C# - Span 全面介绍:探索 .NET 新增的重要组成部分

發(fā)布時(shí)間:2023/12/4 C# 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C# - Span 全面介绍:探索 .NET 新增的重要组成部分 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

假設(shè)要公開(kāi)特殊化排序例程,以就地對(duì)內(nèi)存數(shù)據(jù)執(zhí)行操作。可能要公開(kāi)需要使用數(shù)組的方法,并提供對(duì)相應(yīng) T[] 執(zhí)行操作的實(shí)現(xiàn)。如果方法的調(diào)用方有數(shù)組,且希望對(duì)整個(gè)數(shù)組進(jìn)行排序,這樣做就非常合適。但如果調(diào)用方只想對(duì)部分?jǐn)?shù)組進(jìn)行排序,該怎么辦?可能還要公開(kāi)需要使用偏移和計(jì)數(shù)的重載。但如果要支持的內(nèi)存數(shù)據(jù)不在數(shù)組中,而是來(lái)自本機(jī)代碼(舉個(gè)例子)或位于堆棧上,并且你只有指針和長(zhǎng)度,該怎么辦?如何才能讓編寫(xiě)的排序方法對(duì)內(nèi)存的任意區(qū)域執(zhí)行操作,同時(shí)還對(duì)完整數(shù)組或部分?jǐn)?shù)組以及托管數(shù)組和非托管指針同樣有效?

又例如,假設(shè)要對(duì) System.String 實(shí)現(xiàn)操作,如使用特殊化分析方法。可能要公開(kāi)需要使用字符串的方法,并提供對(duì)字符串執(zhí)行操作的實(shí)現(xiàn)。但如果要支持對(duì)部分字符串執(zhí)行操作,該怎么辦?雖然 String.Substring 可用于分離出僅感興趣的部分,但此操作的成本相對(duì)高昂,涉及字符串分配和內(nèi)存復(fù)制。正如數(shù)組示例中提到的,可以使用偏移和計(jì)數(shù)。但如果調(diào)用方?jīng)]有字符串,而是有 char[],該怎么辦?或者,如果調(diào)用方有 char*(例如為了使用堆棧上某空間而使用 stackalloc 創(chuàng)建的,或通過(guò)調(diào)用本機(jī)代碼而生成的),該怎么辦?如果才能讓編寫(xiě)的分析方法不強(qiáng)制調(diào)用方執(zhí)行任何分配或復(fù)制操作,同時(shí)還對(duì)輸入的類(lèi)型字符串、char[] 和 char* 同樣有效?

在這兩個(gè)示例中,都可以使用不安全代碼和指針,同時(shí)公開(kāi)接受指針和長(zhǎng)度的實(shí)現(xiàn)。不過(guò),這樣一來(lái),就無(wú)法獲取對(duì) .NET 至關(guān)重要的安全保障,并且會(huì)遇到對(duì)大多數(shù) .NET 開(kāi)發(fā)人員而言已成為過(guò)去的問(wèn)題,如緩沖區(qū)溢出和訪問(wèn)沖突。此外,這還會(huì)引發(fā)其他性能損失,如需要在操作期間固定托管對(duì)象,讓檢索的指針一直有效。而且根據(jù)涉及的數(shù)據(jù)類(lèi)型,獲取指針根本就不可行。

此難題還是有解決方法的,即使用 Span<T>。

什么是 Span<T>?

System.Span<T> 是在 .NET 中發(fā)揮關(guān)鍵作用的新值類(lèi)型。使用它,可以表示任意內(nèi)存的相鄰區(qū)域,無(wú)論相應(yīng)內(nèi)存是與托管對(duì)象相關(guān)聯(lián),還是通過(guò)互操作由本機(jī)代碼提供,亦或是位于堆棧上。除了具有上述用途外,它仍能確保安全訪問(wèn)和高性能特性,就像數(shù)組一樣。

例如,可以通過(guò)數(shù)組創(chuàng)建 Span<T>:

var arr = new byte[10]; Span<byte> bytes = arr; // Implicit cast from T[] to Span<T>

隨后,可以輕松高效地創(chuàng)建 Span,以利用 Span 的 Slice 方法重載,僅表示/指向此數(shù)組的子集。隨后,可以為生成的 Span 編制索引,以編寫(xiě)和讀取原始數(shù)組中相關(guān)部分的數(shù)據(jù):

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 IndexOutOfRangeExceptionbytes[2] = 45; // OK
Assert.Equal(arr[2], bytes[2]); Assert.Equal(45, arr[2]);

正如之前提到的,Span 不僅僅只能用于訪問(wèn)數(shù)組和分離出數(shù)組子集。還可用于引用堆棧上的數(shù)據(jù)。例如,

Span<byte> bytes = stackalloc byte[2]; // Using C# 7.2 stackalloc support for spansbytes[0] = 42; bytes[1] = 43; Assert.Equal(42, bytes[0]); Assert.Equal(43, bytes[1]); bytes[2] = 44; // throws IndexOutOfRangeException

更為普遍的是,Span 可用于引用任意指針和長(zhǎng)度(如通過(guò)本機(jī)堆分配的內(nèi)存),如下所示:

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> 索引器利用 C# 7.0 中引入的 C# 語(yǔ)言功能,即引用返回。索引器使用“引用 T”返回類(lèi)型進(jìn)行聲明,其中提供為數(shù)組編制索引的語(yǔ)義,同時(shí)返回對(duì)實(shí)際存儲(chǔ)位置的引用,而不是相應(yīng)位置上存在的副本:

public ref T this[int index] { get { ... } }

通過(guò)示例,可以最明顯地體現(xiàn)這種引用返回類(lèi)型索引器帶來(lái)的影響,如將它與不是引用返回類(lèi)型的 List<T> 索引器進(jìn)行比較。例如:

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> 的第二個(gè)變體為 System.ReadOnlySpan<T>,可啟用只讀訪問(wèn)。此類(lèi)型與 Span<T> 基本類(lèi)似,不同之處在于前者的索引器利用新 C# 7.2 功能來(lái)返回“引用只讀 T”,而不是“引用 T”,這樣就可以處理 System.String 等不可變數(shù)據(jù)類(lèi)型。使用 ReadOnlySpan<T>,可以非常高效地分離字符串,而無(wú)需執(zhí)行分配或復(fù)制操作,如下所示:

string str = "hello, world";string worldString = str.Substring(startIndex: 7, length: 5); // Allocates ReadOnlySpan<char> worldSpan =? str.AsReadOnlySpan().Slice(start: 7, length: 5); // No allocationAssert.Equal('w', worldSpan[0]); worldSpan[0] = 'a'; // Error CS0200: indexer cannot be assigned to

Span 的優(yōu)勢(shì)還有許多,遠(yuǎn)不止已提到的這些。例如,Span 支持 reinterpret_cast 的理念,即可以將 Span<byte> 強(qiáng)制轉(zhuǎn)換為 Span<int>(其中,Span<int> 中的索引 0 映射到 Span<byte> 的前四個(gè)字節(jié))。這樣一來(lái),如果讀取字節(jié)緩沖區(qū),可以安全高效地將它傳遞到對(duì)分組字節(jié)(視作整數(shù))執(zhí)行操作的方法。

如何實(shí)現(xiàn) Span<T>?

開(kāi)發(fā)人員通常無(wú)需了解要使用的庫(kù)是如何實(shí)現(xiàn)的。不過(guò),對(duì)于 Span<T>,對(duì)背后的運(yùn)作機(jī)制詳情至少有一個(gè)基本了解是值得的,因?yàn)檫@些詳情暗含有關(guān)性能和使用約束的相關(guān)信息。

首先,Span<T> 是包含引用和長(zhǎng)度的值類(lèi)型,定義大致如下:

public readonly ref struct Span<T> {private readonly ref T _pointer;private readonly int _length;... }

“引用 T”字段這一概念初看起來(lái)有些奇怪,因?yàn)槠鋵?shí)無(wú)法在 C# 或甚至 MSIL 中聲明“引用 T”字段。不過(guò),Span<T> 實(shí)際上旨在于運(yùn)行時(shí)使用特殊內(nèi)部類(lèi)型,可看作是內(nèi)部實(shí)時(shí) (JIT) 類(lèi)型,由 JIT 為其生成等效的“引用 T”字段。以可能更為熟悉的引用用法為例:

public static void AddOne(ref int value) => value += 1; ...var values = new int[] { 42, 84, 126 }; AddOne(ref values[2]); Assert.Equal(127, values[2]);

此代碼通過(guò)引用傳遞數(shù)組中的槽,這樣(除優(yōu)化外)還可以在堆棧上生成引用 T。Span<T> 中的引用 T 有異曲同工之妙,直接封裝在結(jié)構(gòu)中。直接或間接包含此類(lèi)引用的類(lèi)型被稱(chēng)為類(lèi)似引用的類(lèi)型,C# 7.2 編譯器支持在簽名中使用引用結(jié)構(gòu),從而聲明這種類(lèi)似引用的類(lèi)型。

根據(jù)這一簡(jiǎn)要說(shuō)明,應(yīng)明確兩點(diǎn):

  • Span<T> 的定義方式可確保操作效率與數(shù)組一樣高:為 Span 編制索引無(wú)需通過(guò)計(jì)算來(lái)確定指針開(kāi)頭及其起始偏移,因?yàn)椤耙谩弊侄伪旧硪褜?duì)兩者進(jìn)行了封裝。(相比之下,ArraySegment<T> 有單獨(dú)的偏移字段,這就增加了索引編制和數(shù)據(jù)傳遞操作的成本。)

  • 鑒于類(lèi)似引用的類(lèi)型這一本質(zhì),Span<T> 因其“引用 T”字段而受到一些約束。

  • 第二點(diǎn)帶來(lái)了一些有趣的后果,即導(dǎo)致 .NET 包含第二組相關(guān)的類(lèi)型(由 Memory<T> 主導(dǎo))。

    什么是 Memory<T>?為什么需要它?

    Span<T> 是類(lèi)似引用的類(lèi)型,因?yàn)樗耙谩弊侄?#xff0c;而且“引用”字段不僅可以引用數(shù)組等對(duì)象的開(kāi)頭,還可以引用它們的中間部分:

    var arr = new byte[100]; Span<byte> interiorRef1 = arr.AsSpan().Slice(start: 20); Span<byte> interiorRef2 = new Span<byte>(arr, 20, arr.Length – 20); Span<byte> interiorRef3 =Span<byte>.DangerousCreate(arr, ref arr[20], arr.Length – 20);

    這些引用被稱(chēng)為“內(nèi)部指針”。對(duì)于 .NET 運(yùn)行時(shí)的垃圾回收器,跟蹤這些指針是一項(xiàng)成本相對(duì)高昂的操作。因此,運(yùn)行時(shí)將這些引用約束為僅存在于堆棧上,因?yàn)樗[式規(guī)定了可以存在的內(nèi)部指針數(shù)量下限。

    此外,如前所述,Span<T> 大于計(jì)算機(jī)的字大小;也就是說(shuō),對(duì) Span 執(zhí)行的讀取和寫(xiě)入操作不是原子操作。如果多個(gè)線程同時(shí)對(duì) Span 在堆上的字段執(zhí)行讀取和寫(xiě)入操作,存在“撕裂”風(fēng)險(xiǎn)。 假設(shè)現(xiàn)有一個(gè)已初始化的 Span,其中包含有效引用和值為 50 的相應(yīng) _length。一個(gè)線程開(kāi)始編寫(xiě)新 Span,并且還編寫(xiě)新 _pointer 值。然后,還未將相應(yīng)的 _length 設(shè)置為 20,另一個(gè)線程就開(kāi)始讀取 Span,其中包含新 _pointer 和更長(zhǎng)的舊 _length。

    這樣一來(lái),Span<T> 示例只能存在于堆棧上,而不能存在于堆上。也就是說(shuō),無(wú)法將 Span 裝箱,進(jìn)而無(wú)法將 Span<T> 與現(xiàn)有反射調(diào)用 API(舉個(gè)例子)結(jié)合使用,因?yàn)樗鼈冃枰獔?zhí)行裝箱。這意味著,無(wú)法將 Span<T> 字段封裝在類(lèi)中,甚至也無(wú)法封裝在不類(lèi)似引用的結(jié)構(gòu)中。也就是說(shuō),如果 Span 可能會(huì)隱式成為類(lèi)中的字段,則無(wú)法使用它們。例如,將它們捕獲到 lambda 中,或?qū)⑺鼈儾东@為異步方法或迭代器中的本地字段,因?yàn)檫@些本地字段可能最終會(huì)成為編譯器生成的狀態(tài)機(jī)上的字段。 這還意味著,無(wú)法將 Span<T> 用作泛型參數(shù),因?yàn)轭?lèi)型參數(shù)實(shí)例可能最終會(huì)被裝箱或以其他方式存儲(chǔ)到堆上(暫無(wú)“where T : ref struct”約束)。

    對(duì)于許多方案,尤其是對(duì)于受計(jì)算量限制和同步處理功能,這些限制無(wú)關(guān)緊要。不過(guò),異步功能卻是另一回事。無(wú)論是處理同步操作還是異步操作,本文開(kāi)頭提到的大部分有關(guān)數(shù)組、數(shù)組切片和本機(jī)內(nèi)存等問(wèn)題仍存在。但如果 Span<T> 無(wú)法存儲(chǔ)到堆,因而無(wú)法跨異步操作暫留,那么還有什么解決方法?答案就是 Memory<T>。

    Memory<T> looks very much like an ArraySegment<T>:public readonly struct Memory<T> {private readonly object _object;private readonly int _index;private readonly int _length;... }

    可以通過(guò)數(shù)組創(chuàng)建 Memory<T>,并進(jìn)行切片。這與處理 Span 基本相同,不同之處在于 Memory<T> 是不類(lèi)似引用的結(jié)構(gòu),可以存在于堆上。然后,若要執(zhí)行同步處理,可以從中獲取 Span<T>,例如:

    static async Task<int> ChecksumReadAsync(Memory<byte> buffer, Stream stream) {int bytesRead = await stream.ReadAsync(buffer);return Checksum(buffer.Span.Slice(0, bytesRead));// Or buffer.Slice(0, bytesRead).Span}static int Checksum(Span<byte> buffer) { ... }

    與 Span<T> 和 ReadOnlySpan<T> 一樣,Memory<T> 也有等效的只讀類(lèi)型,即 ReadOnlyMemory<T>。與預(yù)期一樣,它的 Span 屬性返回 ReadOnlySpan<T>。請(qǐng)參閱圖 1,快速概覽在這些類(lèi)型之間進(jìn)行轉(zhuǎn)換的內(nèi)置機(jī)制。

    圖 1:在 Span 相關(guān)類(lèi)型之間進(jìn)行非分配/非復(fù)制轉(zhuǎn)換

    來(lái)自收件人機(jī)制
    ArraySegment<T>Memory<T>隱式強(qiáng)制轉(zhuǎn)換、AsMemory 方法
    ArraySegment<T>ReadOnlyMemory<T>隱式強(qiáng)制轉(zhuǎn)換、AsReadOnlyMemory 方法
    ArraySegment<T>ReadOnlySpan<T>隱式強(qiáng)制轉(zhuǎn)換、AsReadOnlySpan 方法
    ArraySegment<T>Span<T>隱式強(qiáng)制轉(zhuǎn)換、AsSpan 方法
    ArraySegment<T>T[]Array 屬性
    Memory<T>ArraySegment<T>TryGetArray 方法
    Memory<T>ReadOnlyMemory<T>隱式強(qiáng)制轉(zhuǎn)換、AsReadOnlyMemory 方法
    Memory<T>Span<T>Span 屬性
    ReadOnlyMemory<T>ArraySegment<T>DangerousTryGetArray 方法
    ReadOnlyMemory<T>ReadOnlySpan<T>Span 屬性
    ReadOnlySpan<T>ref readonly T索引器 get 取值函數(shù)、封送處理方法
    Span<T>ReadOnlySpan<T>隱式強(qiáng)制轉(zhuǎn)換、AsReadOnlySpan 方法
    Span<T>ref T索引器 get 取值函數(shù)、封送處理方法
    字符串ReadOnlyMemory<char>AsReadOnlyMemory 方法
    字符串ReadOnlySpan<char>隱式強(qiáng)制轉(zhuǎn)換、AsReadOnlySpan 方法
    T[]ArraySegment<T>構(gòu)造函數(shù)、隱式強(qiáng)制轉(zhuǎn)換
    T[]Memory<T>構(gòu)造函數(shù)、隱式強(qiáng)制轉(zhuǎn)換、AsMemory 方法
    T[]ReadOnlyMemory<T>構(gòu)造函數(shù)、隱式強(qiáng)制轉(zhuǎn)換、AsReadOnlyMemory 方法
    T[]ReadOnlySpan<T>構(gòu)造函數(shù)、隱式強(qiáng)制轉(zhuǎn)換、AsReadOnlySpan 方法
    T[]Span<T>構(gòu)造函數(shù)、隱式強(qiáng)制轉(zhuǎn)換、AsSpan 方法
    void*ReadOnlySpan<T>構(gòu)造函數(shù)
    void*Span<T>構(gòu)造函數(shù)

    將會(huì)注意到,Memory<T> 的 _object 字段并未強(qiáng)類(lèi)型化為 T[],而是存儲(chǔ)為對(duì)象。這突出說(shuō)明 Memory<T> 可以包裝數(shù)組以外的內(nèi)容,如 System.Buffers.OwnedMemory<T>。OwnedMemory<T> 是抽象類(lèi),可用于包裝需要密切管理其生存期的數(shù)據(jù),如從池中檢索到的內(nèi)存。此主題更為高級(jí),超出了本文的介紹范圍,但這就是 Memory<T> 的用途所在(例如,用于將指針包裝到本機(jī)內(nèi)存)。ReadOnlyMemory<char> 也可以與字符串結(jié)合使用,就像 ReadOnlySpan<char> 一樣。

    Span<T> 和 Memory<T> 如何與 .NET 庫(kù)集成?

    在上面的 Memory<T> 代碼片段中,將會(huì)注意到傳入 Memory<byte> 的 Stream.ReadAsync 調(diào)用。但如今在 .NET 中,Stream.ReadAsync 被定義為接受 byte[]。它的工作原理是什么?

    為了支持 Span<T> 及其成員,即將向 .NET 添加數(shù)百個(gè)新成員和類(lèi)型。其中大多是現(xiàn)有基于數(shù)組和基于字符串的方法的重載,而另一些則是專(zhuān)注于特定處理方面的全新類(lèi)型。例如,除了包含需要使用字符串的現(xiàn)有重載外,所有原始類(lèi)型(如 Int32)現(xiàn)在都包含接受 ReadOnlySpan<char> 的 Parse 重載。假設(shè)字符串包含兩部分?jǐn)?shù)字(用逗號(hào)隔開(kāi),如“123,456”),且希望分析這部分?jǐn)?shù)字。現(xiàn)在,可以編寫(xiě)如下代碼:

    string input = ...;int commaPos = input.IndexOf(',');int first = int.Parse(input.Substring(0, commaPos));int second = int.Parse(input.Substring(commaPos + 1));

    不過(guò),這會(huì)生成兩個(gè)字符串分配。若要編寫(xiě)高性能代碼,兩個(gè)字符串分配可能就太多了。此時(shí),可以改為編寫(xiě)如下代碼:

    string input = ...; ReadOnlySpan<char> inputSpan = input.AsReadOnlySpan();int commaPos = input.IndexOf(',');int first = int.Parse(inputSpan.Slice(0, commaPos));int second = int.Parse(inputSpan.Slice(commaPos + 1));

    通過(guò)使用基于 Span 的新 Parse 重載,可以在這整個(gè)操作期間避免執(zhí)行分配操作。類(lèi)似分析和格式化方法可用于原始類(lèi)型(如 Int32),其中包括 DateTime、TimeSpan 和 Guid 等核心類(lèi)型,甚至還包括 BigInteger 和 IPAddress 等更高級(jí)別類(lèi)型。

    實(shí)際上,已跨框架添加了許多這樣的方法。從 System.Random 到 System.Text.StringBuilder,再到 System.Net.Socket,這些重載的添加有利于輕松高效地處理 {ReadOnly}Span<T> 和 {ReadOnly}Memory<T>。其中一些甚至帶來(lái)了額外的好處。例如,Stream 現(xiàn)包含以下方法:

    public virtual ValueTask<int> ReadAsync(Memory<byte> destination,CancellationToken cancellationToken = default) { ... }

    將會(huì)注意到,不同于接受 byte[] 并返回 Task<int> 的現(xiàn)有 ReadAsync 方法,此重載不僅接受 Memory<byte>(而不是 byte[]),還返回 ValueTask<int>(而不是 Task<int>)。在以下情況下,ValueTask<T> 是有助于避免執(zhí)行分配操作的結(jié)構(gòu):經(jīng)常要求使用異步方法來(lái)同步返回內(nèi)容,以及不太可能為所有常見(jiàn)返回值緩存已完成任務(wù)。例如,運(yùn)行時(shí)可以為結(jié)果 true 和 false 緩存已完成的 Task<bool>,但無(wú)法為 Task<int> 的所有可能結(jié)果值緩存四十億任務(wù)對(duì)象。

    由于相當(dāng)常見(jiàn)的是 Stream 實(shí)現(xiàn)的緩沖方式讓 ReadAsync 調(diào)用同步完成,因此這一新 ReadAsync 重載返回 ValueTask<int>。也就是說(shuō),同步完成的異步 Stream 讀取操作可以完全避免執(zhí)行分配操作。ValueTask<T> 也用于其他新重載,如 Socket.ReceiveAsync、Socket.SendAsync、WebSocket.ReceiveAsync 和 TextReader.ReadAsync 重載。

    此外,在一些情況下,Span<T> 還支持向框架添加在過(guò)去引發(fā)內(nèi)存安全問(wèn)題的方法。假設(shè)要?jiǎng)?chuàng)建的字符串包含隨機(jī)生成的值(如某類(lèi) ID)。現(xiàn)在,可能會(huì)編寫(xiě)要求分配字符數(shù)組的代碼,如下所示:

    int length = ...; Random rand = ...;var chars = new char[length];for (int i = 0; i < chars.Length; i++) {chars[i] = (char)(rand.Next(0, 10) + '0'); }string id = new string(chars);

    可以改用堆棧分配,甚至能夠利用 Span<char>,這樣就無(wú)需使用不安全代碼。此方法還利用接受 ReadOnlySpan<char> 的新字符串構(gòu)造函數(shù),如下所示:

    int length = ...; Random rand = ...; Span<char> chars = stackalloc char[length];for (int i = 0; i < chars.Length; i++) {chars[i] = (char)(rand.Next(0, 10) + '0'); }string id = new string(chars);

    這樣做更好,因?yàn)楸苊饬硕逊峙?#xff0c;但仍不得不將堆棧上生成的數(shù)據(jù)復(fù)制到字符串中。同樣,只有在所需空間大小對(duì)于堆棧而言足夠小時(shí),此方法才有效。如果長(zhǎng)度較短(如 32 個(gè)字節(jié)),可以使用此方法;但如果長(zhǎng)度為數(shù)千字節(jié),很容易就會(huì)引發(fā)堆棧溢出問(wèn)題。如果可以改為直接寫(xiě)入字符串的內(nèi)存,該怎么辦?Span<T> 可以實(shí)現(xiàn)此目的。除了包含新構(gòu)造函數(shù)以外,字符串現(xiàn)在還包含 Create 方法:

    public static string Create<TState>(int length, TState state, SpanAction<char, TState> action); ...public delegate void SpanAction<T, in TArg>(Span<T> span, TArg arg);

    實(shí)現(xiàn)此方法是為了分配字符串,并分發(fā)可寫(xiě) Span,執(zhí)行寫(xiě)入操作后可以在構(gòu)造字符串的同時(shí)填寫(xiě)字符串的內(nèi)容。請(qǐng)注意,在此示例中,Span<T> 的僅限堆棧這一本質(zhì)非常有用,因?yàn)榭梢员WC在字符串的構(gòu)造函數(shù)完成前 Span(引用字符串的內(nèi)部存儲(chǔ))就不存在,這樣便無(wú)法在構(gòu)造完成后使用 Span 改變字符串了:

    int length = ...; Random rand = ...;string id = string.Create(length, rand, (Span<char> chars, Random r) => {for (int i = 0; chars.Length; i++){chars[i] = (char)(r.Next(0, 10) + '0');} });

    現(xiàn)在,不僅避免了分配操作,還可以直接寫(xiě)入字符串在堆上的內(nèi)存,即也避免了復(fù)制操作,且不受堆棧大小限制的約束。

    除了核心框架類(lèi)型有新成員外,我們還正在積極開(kāi)發(fā)許多可與 Span 結(jié)合使用的新 .NET 類(lèi)型,從而在特定方案中實(shí)現(xiàn)高效處理。例如,對(duì)于要編寫(xiě)高性能微服務(wù)和處理大量文本的網(wǎng)站的開(kāi)發(fā)人員,如果在使用 UTF-8 時(shí)無(wú)需編碼和解碼字符串,則性能會(huì)大大提升。為此,我們即將添加 System.Buffers.Text.Base64、System.Buffers.Text.Utf8Parser 和 System.Buffers.Text.Utf8Formatter 等新類(lèi)型。這些類(lèi)型對(duì)字節(jié) Span 執(zhí)行操作,不僅避免了 Unicode 編碼和解碼,還能夠處理在各種網(wǎng)絡(luò)堆棧的最低級(jí)別中常見(jiàn)的本機(jī)緩沖:

    ReadOnlySpan<byte> utf8Text = ...;if (!Utf8Parser.TryParse(utf8Text, out Guid value,out int bytesConsumed, standardFormat = 'P'))throw new InvalidDataException();

    所有此類(lèi)功能不僅僅只用于公共使用用途;框架本身也可以利用這些基于 Span<T> 和基于 Memory<T> 的新方法來(lái)提升性能。跨 .NET Core 調(diào)用網(wǎng)站已切換為使用新的 ReadAsync 重載,以避免不必要的分配操作。分析過(guò)去是通過(guò)分配子字符串完成,現(xiàn)在可以避免執(zhí)行分配操作。甚至 Rfc2898DeriveBytes 等間隙類(lèi)型也實(shí)際運(yùn)用了此功能,利用 System.Security.Cryptography.Hash-Algorithm 上基于 Span<byte> 的新 TryComputeHash 方法顯著減少分配操作量(每次算法迭代的字節(jié)數(shù)組,可能迭代數(shù)千次)和提升吞吐量。

    這并未止步于核心 .NET 庫(kù)一級(jí),而是繼續(xù)全面影響堆棧。ASP.NET Core 現(xiàn)在嚴(yán)重依賴(lài) Span;例如,在 Span 基礎(chǔ)之上編寫(xiě) Kestrel 服務(wù)器的 HTTP 分析程序。Span 今后可能會(huì)通過(guò)較低級(jí)別 ASP.NET Core 中的公共 API 公開(kāi),如在它的中間件管道中。

    .NET 運(yùn)行時(shí)又如何呢?

    .NET 運(yùn)行時(shí)提供安全保障的方法之一是,確保為數(shù)組編制的索引不超出數(shù)組的長(zhǎng)度,這種做法稱(chēng)為“邊界檢查”。例如,以下面這個(gè)方法為例:

    [MethodImpl(MethodImplOptions.NoInlining)]static int Return4th(int[] data) => data[3];

    在我撰寫(xiě)本文使用的 x64 計(jì)算機(jī)上,針對(duì)此方法生成的程序集如下所示:

    sub????? rsp, 40cmp????? dword ptr [rcx+8], 3jbe????? SHORT G_M22714_IG04mov????? eax, dword ptr [rcx+28]add????? rsp, 40ret G_M22714_IG04:call ????CORINFO_HELP_RNGCHKFAILint3

    cmp 指令將數(shù)據(jù)數(shù)組的長(zhǎng)度與索引 3 進(jìn)行比較。如果 3 超出范圍(異常拋出),后續(xù) jbe 指令會(huì)轉(zhuǎn)到范圍檢查失敗例程。雖然 JIT 需要生成代碼,以確保此類(lèi)訪問(wèn)不會(huì)超出數(shù)組邊界,但這并不意味著每個(gè)數(shù)組訪問(wèn)都需要進(jìn)行邊界檢查。以下面的 Sum 方法為例:

    static int Sum(int[] data) {int sum = 0;for (int i = 0; i < data.Length; i++) sum += data[i];return sum; }

    雖然 JIT 此時(shí)需要生成代碼,以確保對(duì) data[i] 的訪問(wèn)不超出數(shù)組邊界,但因?yàn)?JIT 能夠通過(guò)循環(huán)結(jié)構(gòu)判斷 i 一直在范圍內(nèi)(循環(huán)從頭到尾遍歷每個(gè)元素),所以 JIT 可以?xún)?yōu)化為不對(duì)數(shù)組進(jìn)行邊界檢查。因此,針對(duì)循環(huán)生成的程序集代碼如下所示:

    G_M33811_IG03:movsxd?? r9, edxadd????? eax, dword ptr [rcx+4*r9+16]inc????? edxcmp????? r8d, edxjg?????? SHORT G_M33811_IG03

    雖然 cmp 指令仍在循環(huán)中,但只需將 i 值(存儲(chǔ)在 edx 寄存器中)與數(shù)組長(zhǎng)度(存儲(chǔ)在 r8d 寄存器中)進(jìn)行比較,無(wú)需額外進(jìn)行邊界檢查。

    運(yùn)行時(shí)向 Span(Span<T> 和 ReadOnlySpan<T>)應(yīng)用類(lèi)似優(yōu)化。將上面的示例與下面的代碼進(jìn)行比較,唯一的變化是參數(shù)類(lèi)型:

    static int Sum(Span<int> data) {int sum = 0;for (int i = 0; i < data.Length; i++) sum += data[i];return sum; }

    針對(duì)此代碼生成的程序集幾乎完全相同:

    G_M33812_IG03:movsxd?? r9, r8dadd????? ecx, dword ptr [rax+4*r9]inc????? r8dcmp????? r8d, edxjl?????? SHORT G_M33812_IG03

    程序集代碼如此相似,部分是因?yàn)椴挥眠M(jìn)行邊界檢查。此外,同樣重要的是 JIT 將 Span 索引器識(shí)別為內(nèi)部類(lèi)型,即 JIT 為索引器生成特殊代碼,而不是將它的實(shí)際 IL 代碼轉(zhuǎn)換為程序集。

    所有這些都是為了說(shuō)明運(yùn)行時(shí)可以為 Span 應(yīng)用與數(shù)組相同的優(yōu)化類(lèi)型,讓 Span 成為高效的數(shù)據(jù)訪問(wèn)機(jī)制。如需了解更多詳情,請(qǐng)參閱?bit.ly/2zywvyI?上的博客文章。

    C# 語(yǔ)言和編譯器又如何呢?

    我已暗示,添加到 C# 語(yǔ)言和編譯器的功能有助于讓 Span<T> 成為 .NET 中的一流成員。C# 7.2 的多項(xiàng)功能都與 Span 相關(guān)(實(shí)際上,C# 7.2 編譯器必須使用 Span<T>)。接下來(lái),將介紹三個(gè)此類(lèi)功能。

    引用結(jié)構(gòu)。如前所述,Span<T> 是類(lèi)似引用的類(lèi)型,自版本 7.2 起在 C# 中公開(kāi)為引用結(jié)構(gòu)。通過(guò)將引用關(guān)鍵字置于結(jié)構(gòu)前,可以指示 C# 編譯器將其他引用結(jié)構(gòu)類(lèi)型(如 Span<T>)用作字段,這樣做還會(huì)注冊(cè)要分配給類(lèi)型的相關(guān)約束。例如,若要為 Span<T> 編寫(xiě)結(jié)構(gòu)枚舉器,枚舉器需要存儲(chǔ) Span<T>,因此它本身必須是引用結(jié)構(gòu),如下所示:

    public ref struct Enumerator {private readonly Span<char> _span;private int _index;... }

    Span 的 stackalloc 初始化。在舊版 C# 中,只能將 stackalloc 的結(jié)果存儲(chǔ)到指針本地變量中。自 C# 7.2 起,現(xiàn)在可以在表達(dá)式中使用 stackalloc,并能定目標(biāo)到 Span,而不使用不安全關(guān)鍵字。因?yàn)?#xff0c;無(wú)需編寫(xiě):

    Span<byte> bytes;unsafe{byte* tmp = stackalloc byte[length];bytes = new Span<byte>(tmp, length); }

    只需編寫(xiě):

    Span<byte> bytes = stackalloc byte[length];

    如果需要一些空間來(lái)執(zhí)行操作,但又希望避免分配相對(duì)較小的堆內(nèi)存,此代碼就非常有用。過(guò)去有以下兩種選擇:

    • 編寫(xiě)兩個(gè)完全不同的代碼路徑,對(duì)基于堆棧的內(nèi)存和基于堆的內(nèi)存執(zhí)行分配和操作。

    • 固定與托管分配相關(guān)聯(lián)的內(nèi)存,再委托到實(shí)現(xiàn),實(shí)現(xiàn)也用于基于堆棧的內(nèi)存,并通過(guò)不安全代碼中的指針控制進(jìn)行編寫(xiě)。

    現(xiàn)在,不使用代碼復(fù)制,即可完成相同的操作,而且還可以使用安全代碼和最簡(jiǎn)單的操作:

    Span<byte> bytes = length <= 128 ? stackalloc byte[length] : new byte[length]; ... // Code that operates on the Span<byte>

    Span 使用驗(yàn)證。因?yàn)?Span 可以引用可能與給定堆棧幀相關(guān)聯(lián)的數(shù)據(jù),所以傳遞 Span 可能存在危險(xiǎn),此操作可能會(huì)引用不再有效的內(nèi)存。例如,假設(shè)方法嘗試執(zhí)行以下操作:

    static Span<char> FormatGuid(Guid guid) {Span<char> chars = stackalloc char[100];bool formatted = guid.TryFormat(chars, out int charsWritten, "d");Debug.Assert(formatted);return chars.Slice(0, charsWritten); // Uh oh}

    此時(shí),空間從堆棧進(jìn)行分配,然后嘗試返回對(duì)此空間的引用,但在返回的同時(shí),此空間不再可用。幸運(yùn)的是,C# 編譯器使用引用結(jié)構(gòu)檢測(cè)此類(lèi)無(wú)效使用,并會(huì)停止編譯,同時(shí)顯示以下錯(cuò)誤:

    錯(cuò)誤 CS8352:無(wú)法在此上下文中使用本地“字符”,因?yàn)樗赡軙?huì)在聲明范圍外公開(kāi)引用的變量

    接下來(lái)會(huì)怎樣呢?

    本文介紹的類(lèi)型、方法、運(yùn)行時(shí)優(yōu)化和其他元素即將順利添加到 .NET Core 2.1 中。之后,我預(yù)計(jì)它們會(huì)全面影響 .NET Framework。核心類(lèi)型(如 Span<T>)和新類(lèi)型(如 Utf8Parser)也即將順利添加到與 .NET Standard 1.1 兼容的 System.Memory.dll 包中。這樣一來(lái),相關(guān)功能將適用于現(xiàn)有 .NET Framework 和 .NET Core 版本,盡管在內(nèi)置于平臺(tái)時(shí)沒(méi)有實(shí)現(xiàn)一些優(yōu)化。現(xiàn)在,可以試用此包的預(yù)覽版,只需添加對(duì) NuGet 上 System.Memory.dll 包的引用即可。

    當(dāng)然,請(qǐng)注意,當(dāng)前預(yù)覽版與實(shí)際發(fā)布的穩(wěn)定版之間可能會(huì)有重大變革。此類(lèi)變革很大程度上源于像你這樣的開(kāi)發(fā)人員在試用功能集時(shí)提供的反饋。因此,請(qǐng)?jiān)囉妙A(yù)覽版,并關(guān)注?github.com/dotnet/coreclr?和?github.com/dotnet/corefx?存儲(chǔ)庫(kù),以掌握最新動(dòng)態(tài)。此外,有關(guān)文檔,還可以訪問(wèn)?aka.ms/ref72。

    總的來(lái)說(shuō),此功能集能否取得成功依賴(lài)開(kāi)發(fā)人員試用預(yù)覽版、提供反饋以及利用這些類(lèi)型生成自己的庫(kù),所有這些都是為了能夠在新式 .NET 程序中高效安全地訪問(wèn)內(nèi)存。我們熱切期待聆聽(tīng)大家的使用體驗(yàn)反饋,最好能夠與大家一起在 GitHub 上進(jìn)一步改進(jìn) .NET。

    原文:https://msdn.microsoft.com/zh-cn/magazine/mt814808


    .NET社區(qū)新聞,深度好文,歡迎訪問(wèn)公眾號(hào)文章匯總 http://www.csharpkit.com

    總結(jié)

    以上是生活随笔為你收集整理的C# - Span 全面介绍:探索 .NET 新增的重要组成部分的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。