2018 年 1 月

第 33 卷,第 1 期

本文章是由機器翻譯。

C# - 全面了解 Span:探索新的 .NET 重要支援

Stephen Toub |2018 年 1 月

假設您要公開特定的排序常式運作就地升級記憶體中的資料。您可能會公開採用陣列的方法,並提供該 T [] 上運作的實作。如果方法的呼叫端具有陣列,並想要排序,則整體陣列,但是如果呼叫端只希望部分排序,太好了嗎?您可能再也公開所需位移和計數的多載。但如果您想要支援未在陣列中,但相反地,例如來自原生程式碼,或都是放在堆疊的記憶體中的資料,您只有一個指標和長度?您可以撰寫您的排序方法,在操作的記憶體,這類任意區域,並還執行地一樣良好與完整的陣列或陣列的子集和也運作地一樣良好 managed 的陣列和未受管理的指標?

或舉另一個例子。您正在透過 System.String,實作作業,例如特定的剖析方法。您可能會公開方法可接受 string,並提供字串操作的實作。但您想要支援在該字串的子集的該怎麼辦?String.Substring 無法用於切割的片段,很有趣,但是,這是相當耗費資源的作業,涉及字串配置和記憶體內部複本。您可以如所述,在陣列的範例中,取得位移和計數,但是如果呼叫端沒有字串,而是會具有 char []?或如果呼叫端具有 char *,建立與使用一些空間在堆疊上,或做為原生程式碼呼叫的結果 stackalloc 類嗎?無法書寫您剖析方法時,未強制執行任何配置或複製,呼叫端,還執行同樣的方式與輸入字串型別,char [] 和 char *?

在這兩種情況下,您可能無法使用不安全的程式碼和指標,公開接受指標和長度的實作。不過,就不是.NET 的核心安全性保證,並開啟時,您最多緩衝區滿溢和存取違規,大部分的.NET 開發人員會事 of 過去等問題。它也邀請其他效能的負面影響,例如需要在作業期間釘選受管理的物件,以便您擷取的指標就持續有效。根據所涉及的資料類型,完全取得指標可能不太實用。

有解答這個問題,而且其名稱是範圍 < T >。

< T > 的範圍是什麼?

< T > System.Span 是新的實值類型的核心.NET。它可讓任意的記憶體,不論是否記憶體為受管理物件相關聯,由原生程式碼透過 interop,提供在堆疊上的連續區域的表示法。而不需同時又能提供與陣列類似的效能特性的安全存取。

例如,您可以建立 < T > 的範圍從陣列:

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

從該處,您可以輕鬆又有效率地建立代表/點來只子集這個陣列中的範圍使用的範圍配量方法多載。從該處,您可以索引到產生的範圍來撰寫和閱讀相關的部分目的原始陣列中的資料:

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<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); }

< T > 範圍索引子會利用 C# 語言功能,在 C# 呼叫 ref 傳回 7.0 中導入。索引子的宣告與"ref T"傳回類型,提供類似陣列中進行索引,傳回的實際儲存位置的參考而不是傳回一份什麼居住在該位置的語意:

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

這個 ref 傳回索引子的影響是最明顯透過範例中,例如藉由比較它與 < T > 清單中索引子,這不是 ref 傳回。以下為範例:

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

第二個變數的範圍 < T >,稱為 System.ReadOnlySpan < T >,可讓唯讀存取權。除了其索引子會利用新的 C# 7.2 功能,而不是"ref T,"傳回"ref readonly T",此類型是如同範圍 < T >,讓它使用像 System.String 的不可變的資料類型。ReadOnlySpan < T > 可讓非常有效率的配量字串而不需配置或複製,如下所示:

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

範圍提供許多優點之外,已所述。例如,跨越概念的轉換的向量的轉換,這表示您可以轉型為 (其中 < int > 的範圍中第 0 個索引對應到第四個位元組的範圍 < 位元組 >) 的範圍 < int > 範圍 < 位元組 > 的支援。如果您要讀取的位元組緩衝區這樣一來,您可以將它做為群組位元組 int 安全且有效率的方法。

實作範圍 < T > 的方式

開發人員通常不需要了解他們所使用的程式庫的實作方式。然而,在範圍內 < T >,它是值得必須至少有基本的了解詳細資訊,以這些詳細資料的項目代表其效能和其使用方法限制的相關。

首先,範圍 < T > 是實值類型,其中包含 ref 和長度,大約定義,如下所示:

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

Ref T 欄位的概念可能很奇怪一開始,事實上,其中一個實際上不能宣告 C# 中,或甚至 MSIL ref T 欄位。但實際上寫入範圍 < T > 與產生它相當於 ref T 欄位 JIT 視為在 just-in-time (JIT) 內建函式,執行階段中使用特殊的內部型別。請考慮可能 ref 使用量更為熟悉:

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]);

此程式碼陣列中的位置傳址方式傳遞,使得在堆疊上欄位有 ref T (擱置在一旁最佳化)。在 < T > 的範圍中的參考 T 是相同的概念,只是封裝結構內。直接或間接包含這類 refs 類型稱為 ref 類似的類型和 C# 7.2 編譯器允許這類 ref 類似類型的宣告簽章中使用 ref 結構。

從這個簡短的描述中,應該很清楚兩件事:

  1. 這類的作業可能會與在陣列上為有效的方式來定義範圍 < T > 是: 範圍中進行索引不需要計算來判斷開頭的指標以及其起始的位移,從 ref 欄位本身已封裝兩者。(相反地,ArraySegment < T > 有個別的位移的欄位中,讓您更耗費資源給索引和傳遞)。
  2. 做為 ref 類似類型的範圍 < T > 的本質會帶來一些條件約束,因為其 ref T 欄位。

此第二個項目有一些有趣的細節導致.NET 包含第二個和相關的型別,由 < T > 的記憶體所引導集。

什麼是 < T > 的記憶體,以及您為什麼需要它?

範圍 < T > 才類似 ref 的型別內含 ref 欄位,以及 ref 欄位可以參考的物件陣列,例如開頭不僅中間的它們:

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);

這些參考會呼叫內部指標,並追蹤它們是.NET 執行階段的記憶體回收行程相當耗費資源的作業。因此,執行階段會限制這些 refs 只存留在堆疊上,因為它提供隱含的低限制的內部指標可能存在的數目。

此外,如先前所示的範圍 < T > 大於機器的文字大小,這表示讀取和寫入的範圍不可部分完成的作業。如果多個執行緒的讀取和寫入的範圍欄位堆積在相同的時間,會有風險的 「 分裂 」。 假設已初始化的範圍,其中包含有效的參考和 50 對應 _length。有一個執行緒啟動覆寫它的新範圍,並取得延展寫入新 _pointer 值。然後,它可以將對應 _length 設為 20 之前,第二個執行緒會讀取的範圍內,包括新 _pointer 但舊 (和較長) _length。

如此一來,在堆疊上,不在堆積上只可以即時範圍 < T > 執行個體。這表示您無法 box 處理範圍 (並因此無法使用範圍 < T > 使用現有的反映叫用應用程式開發介面,例如,因為它們需要 boxing)。這表示您不能在類別中,或甚至非 ref 類似結構的範圍 < T > 欄位。這表示您無法使用範圍中位置可能會隱含地陷入欄位上的類別,例如擷取到 lambda,或做為非同步方法或迭代器中的區域變數 (如這些 「 區域變數 」 可能會被編譯器產生的狀態機器上的欄位。) 這也表示您無法使用範圍 < T > 做為泛型引數,因為該型別引數的執行個體最後可能會開始進行 boxed 處理或否則儲存堆積 (而且沒有目前沒有 「 其中 T: ref 結構 」 可用的條件約束)。

這些限制適物質以許多情形來說特別計算繫結和同步處理函式。但非同步功能是另一個故事。大部分的陣列、 陣列配量、 原生記憶體等周圍本文開頭所指出的問題存在是否處理與同步或非同步作業。棒的是,如果範圍 < T > 無法存放到堆積,因此無法在非同步作業之間保存答案是什麼?< 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;
  ...
}

您可以建立從陣列的記憶體 < T >,並就像您會在時間範圍,但這 (非 ref-類似) 結構,可以在堆積上的即時配量。然後,當您想要進行同步處理,您可以取得 < 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) { ... }

就像範圍 < T > 與 < T > ReadOnlySpan,記憶體 < T > 具有唯讀同等權限,ReadOnlyMemory < T >。而如您所預期,其範圍的屬性會傳回 ReadOnlySpan < T >。請參閱圖 1針對這些類型之間進行轉換的內建機制的簡短摘要。

圖 1 非配置/非-複製轉換之間範圍相關的類型

寄件人 收件人 機制
ArraySegment < T > 記憶體 < T > 隱含轉型,AsMemory 方法
ArraySegment < T > ReadOnlyMemory < T > 隱含轉型,AsReadOnlyMemory 方法
ArraySegment < T > ReadOnlySpan < T > 隱含轉型,AsReadOnlySpan 方法
ArraySegment < T > 範圍 < T > 隱含轉型,AsSpan 方法
ArraySegment < T > T] 陣列屬性
記憶體 < T > ArraySegment < T > TryGetArray 方法
記憶體 < T > ReadOnlyMemory < T > 隱含轉型,AsReadOnlyMemory 方法
記憶體 < T > 範圍 < T > 範圍的屬性
ReadOnlyMemory < T > ArraySegment < T > DangerousTryGetArray 方法
ReadOnlyMemory < T > ReadOnlySpan < T > 範圍的屬性
ReadOnlySpan < T > ref readonly T 索引子 get 存取子,封送處理方法
範圍 < T > ReadOnlySpan < T > 隱含轉型,AsReadOnlySpan 方法
範圍 < T > ref T 索引子 get 存取子,封送處理方法
String ReadOnlyMemory < 字元 > AsReadOnlyMemory 方法
String ReadOnlySpan < 字元 > 隱含轉型,AsReadOnlySpan 方法
T] ArraySegment < T > 建構函式,隱含轉型
T] 記憶體 < T > 建構函式,隱含轉型,AsMemory 方法
T] ReadOnlyMemory < T > 建構函式,隱含轉型,AsReadOnlyMemory 方法
T] ReadOnlySpan < T > 建構函式,隱含轉型,AsReadOnlySpan 方法
T] 範圍 < T > 建構函式,隱含轉型,AsSpan 方法
void * ReadOnlySpan < T > 建構函式
void * 範圍 < T > 建構函式

您會注意到的記憶體 < T > _object 欄位不強類型為 T []。相反地,它會儲存為物件。反白顯示 < T > 的記憶體可以換行的陣列,例如 System.Buffers.OwnedMemory < T > 以外的項目。< T > OwnedMemory 是抽象類別,可以用來包裝必須要有其存留期緊密管理,例如記憶體集區中擷取資料。更進階主題超出本文的範圍,但它是如何記憶體 < T > 可用,例如自動換行到原生記憶體的指標。ReadOnlyMemory < 字元 > 也可用透過字串,就如同可以 ReadOnlySpan < 字元 >。

如何範圍 < T > 和 < T > 記憶體整合與.NET 程式庫?

在先前的記憶體 < T > 程式碼片段中,您會發現 Stream.ReadAsync 傳遞 < 位元組 > 在記憶體中的呼叫。但在今天.NET Stream.ReadAsync 定義為接受 byte []。究竟是怎麼運作的?

為了支援 < T > 的範圍和朋友,透過.NET 新增數百個新成員和類型。其中有許多現有陣列為基礎的多載,以字串為基礎的方法,有些則是全新的類型,將焦點放在特定區域的處理。例如,像 Int32 的所有基本型別現在有接受 ReadOnlySpan < 字元 > 除了現有的多載採用字串的剖析多載。假設您預期字串,包含兩個數字 (例如"123456"),以逗號分隔,和您想要剖析出這兩個數字的情況。現在您可以撰寫類似的程式碼:

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

不過,會產生兩個字串配置。如果您要撰寫重視效能的程式碼,可能有兩個字串配置太多。相反地,現在您可以撰寫此:

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));

藉由使用新的範圍為基礎的剖析多載,您已完成這整個作業配置釋放。類似格式化和剖析方法透過核心類型和一樣 DateTime、 TimeSpan 和 Guid,甚至到較高層級的型別,如 BigInteger 和 IPAddress 存在基本類型,例如 Int32 的設定。

事實上,許多這類方法已加入跨架構。從要 System.Net.Sockets 的 System.Text.StringBuilder System.Random,既簡單又有效率,讓使用 {ReadOnly} 範圍 < T > 和 < T > 的 {ReadOnly} 記憶體已加入多載。即使這些部分執行與其額外的好處。例如,資料流現在具有這個方法:

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

您會發現,不同於現有 ReadAsync 方法接受 byte [],並傳回 < int > 的工作,這個多載而不是 byte [],接受的記憶體 < 位元組 > 不僅 ValueTask < int > 也會傳回而不是 < int > 的工作。< T > ValueTask 是結構,可協助避免配置的非同步方法經常預期會傳回同步執行,而且也不太可能我們可以快取所有常見的傳回值已完成的工作。例如,執行階段可以快取已完成的工作 < bool > 的結果為 true,一個供的結果為 false,但它無法快取 < int > 工作的所有可能的結果值的四個 10 億個工作物件。

因為它是很常見的資料流的實作所要緩衝中的方式可以讓 ReadAsync 完成以同步方式呼叫,這個新的 ReadAsync 多載會傳回 ValueTask < int >。非同步資料流讀取作業同步完成,這表示可能會完全釋放配置。ValueTask < T > 也用於其他新的多載,例如在 Socket.ReceiveAsync、 Socket.SendAsync、 WebSocket.ReceiveAsync 和 TextReader.ReadAsync 的多載。

此外,還有範圍 < T >,讓此架構能夠包含方法,在過去引發記憶體安全性考量的地方。假設您要建立包含隨機產生的值,例如特定種類的識別碼的字串。您可以撰寫程式碼需要配置 char 今天的陣列,像這樣:

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);

可以改為使用堆疊配置,即使利用範圍 < 字元 >,以避免需要使用 unsafe 程式碼。這個方法也會利用新字串建構函式可接受 ReadOnlySpan < 字元 >,就像這樣:

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);

這是更好,您已避免堆積配置的但仍會強迫將在堆疊產生的資料複製到字串。所需的空間量很小的堆疊,也只適用於這種方法。如果長度短,32 個位元組,不過沒有關係,這類似,但它是數千個位元組,可能容易導致堆疊溢位狀況。如果您無法寫入字串的記憶體直接改為嗎?範圍 < T > 可讓您執行此作業。字串的新建構函式,除了字串現在也具有 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);

實作這個方法會配置字串,然後送出的可寫入的範圍,以便填滿內容中的字串會在建構時可以寫入。請注意,僅限堆疊範圍 < T > 的本質是有幫助在此情況下,保證 (也就是字串的內部儲存體) 的範圍會中止存在字串的建構函式完成之前,可讓您執行無法使用可變動的範圍字串建構完成之後:

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');
  }
});

現在,您不僅避免配置,您要撰寫直接插入字串的記憶體堆積上,這表示您也要避免複製,而且您不受限於堆疊的大小限制。

超出 core framework 型別取得新的成員,才能使用可在特定案例中有效的處理範圍正在開發許多新的.NET 類型。比方說,如果不一定要以編碼和解碼的字串,在 utf-8 中工作時,就贏了開發人員想要撰寫高效能 microservices 和網站大量使用中的文字處理可以獲得顯著的效能。若要啟用此功能,新增新的類型,例如 System.Buffers.Text.Base64、 System.Buffers.Text.Utf8Parser 和 System.Buffers.Text.Utf8Formatter。這些作業上範圍的位元組,這可避免 Unicode 編碼和解碼,不僅可讓它們使用不同的網路堆疊的最低層級中常見的原生緩衝區:

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

這項功能不只是針對公用耗用量。而是架構本身是能夠利用這些新的範圍 < T >-基礎和記憶體 < T >-基礎方法,以提升效能。.NET Core 呼叫站台已切換為使用新的 ReadAsync 多載來避免不必要的配置。已完成剖析,現在配置子字串,藉以利用配置釋放剖析。即使 niche 類型像 Rfc2898DeriveBytes 收到執行的動作,利用新的範圍 < 位元組 >-根據怪異節省配置 (位元組陣列每個 System.Security.Cryptography.HashAlgorithm TryComputeHash 方法反覆項目的演算法,可能會重複數千次),以及輸送量改進。

這不會阻止核心.NET 程式庫; 層級它會繼續直到堆疊。ASP.NET Core 現在具有大量相依性範圍,例如,與 Kestrel 伺服器的 HTTP 剖析器在其上寫入。未來,很可能範圍會公開從 ASP.NET 核心的較低層級中的公用 Api 這類應用程式的中介軟體管線中。

.NET 執行階段呢?

其中一種.NET 執行階段提供安全性是透過確保陣列編製索引的方式不允許將超出此作法稱為繫結檢查陣列的長度。例如,請考慮這個方法:

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

在 x64 電腦我我輸入本文中,產生的組件的這個方法會類似如下:

sub      rsp, 40
       cmp      dword ptr [rcx+8], 3
       jbe      SHORT G_M22714_IG04
       mov      eax, dword ptr [rcx+28]
       add      rsp, 40
       ret
G_M22714_IG04:
       call     CORINFO_HELP_RNGCHKFAIL
       int3

該 cmp 指令在比對索引 3 的資料陣列的長度和後續 jbe 指令再跳過範圍檢查失敗常式至 3 時超出範圍 (適用於擲回例外狀況)。JIT 需要產生程式碼,以確保這類存取不超出界限的陣列,但這並不表示每個個別的陣列存取需要的繫結的檢查。請考慮這個總和方法:

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

JIT 需求來產生程式碼,以確保資料 [i] 存取不超出界限,但因為迴圈結構中所見 JIT i 一律會是陣列的的範圍 (迴圈逐一查看每個項目從開始到結束)JIT 最佳化會去除在陣列上的繫結檢查。因此,迴圈產生的組件程式碼看起來如下所示:

G_M33811_IG03:
       movsxd   r9, edx
       add      eax, dword ptr [rcx+4*r9+16]
       inc      edx
       cmp      r8d, edx
       jg       SHORT G_M33811_IG03

Cmp 指令仍處於迴圈,而是要比較的值陣列 (如儲存 r8d 暫存器中); 的長度對 i (如同儲存在 edx 暫存器)沒有其他界限檢查。

執行階段套用類似的最佳化,以跨越 (範圍 < T > 和 < T > ReadOnlySpan)。比較下列程式碼,其中的唯一變更是參數型別先前的範例:

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

此程式碼產生的組件是幾乎完全相同:

G_M33812_IG03:
       movsxd   r9, r8d
       add      ecx, dword ptr [rax+4*r9]
       inc      r8d
       cmp      r8d, edx
       jl       SHORT G_M33812_IG03

組譯程式碼很類似部分由於刪除界限檢查。但也相關 JIT 辨識範圍索引子的作為內建,這表示 JIT 產生則為索引子的特殊程式碼,而不是實際的 IL 程式碼轉譯為組件。

這是要說明執行階段可以套用的範圍相同的最佳化,它會為陣列類型,請進行跨越有效率的機制,以存取資料。部落格文章中有更多詳細資料bit.ly/2zywvyI

關於 C# 語言和編譯器什麼?

您已經提到加入 C# 語言和編譯器,讓範圍 < T > 在.NET 中的第一級棒的功能。C# 7.2 的數個功能範圍 (和相關事實上 7.2 C# 編譯器都必須使用範圍 < T >)。讓我們看看這類三項功能。

Ref 結構。如前文所述,範圍 < T > 是 ref 類似的類型,在 C# 版 7.2 ref 結構公開。將放入 ref 關鍵字之前結構告訴 C# 編譯器可讓您使用其他 ref 結構類範圍 < T > 的類型做為欄位,而且這樣做也註冊要指派給您的型別相關聯的條件約束。例如,如果您想要撰寫 < T > 的範圍列舉值的結構,列舉程式必須儲存 < T > 的範圍,因此,會本身需要 ref 結構表示,像這樣:

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

Stackalloc 初始化的範圍。在舊版的 C# 中,stackalloc 結果只可以儲存指標的區域變數。為準,C# 7.2,stackalloc 現在可以作為運算式的一部分,而可鎖定目標範圍內,並不使用 unsafe 關鍵字,即可完成。因此,反而比撰寫:

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

您可以只撰寫:

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

這也是在您需要一些可用空間,執行作業,但想要避免針對較小的大小配置堆積記憶體的情況下非常有用。您先前兩個選擇:

  • 撰寫兩個完全不同的程式碼路徑,配置和操作堆疊為基礎的記憶體和堆積為基礎的記憶體上。
  • 釘選的受管理的配置相關聯的記憶體,然後委派也可用於堆疊為基礎的記憶體和使用 unsafe 程式碼中的指標操作撰寫的實作。

沒有程式碼重複使用安全的程式碼與最小儀式現在,來完成相同的動作:

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

跨越使用驗證。範圍可以參考可能會與特定的堆疊框架相關聯的資料,因為它可以是危險傳遞周圍的範圍可能會讓參考不再有效的記憶體的方式。例如,假設有嘗試執行下列作業的方法:

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
}

這裡所配置的空間堆疊,再嘗試傳回該空間中,但您傳回的目前的參考,從該空間將無法再使用。好 C# 編譯器會偵測這類 ref 結構具有無效的使用,並會編譯失敗且發生錯誤:

錯誤 CS8352:無法使用本機選取 'chars' 在此內容中因為它可能會公開其宣告範圍之外的參考的變數

下一步是什麼?

型別、 方法、 執行階段最佳化,以及這裡所討論的其他項目會包含在.NET Core 2.1 的播放軌上。在這之後,應該會有它們在.NET framework 進行的方式。範圍 < T >,類似的核心類型,以及像 Utf8Parser,新的類型也是可使用與.NET 標準 1.1 相容的 System.Memory.dll 封裝中的播放軌上。會將功能提供給現有的發行版本的.NET Framework 和.NET Core 雖然不含某些內建的平台時,實作的最佳化。此套件的預覽是可供您試用今天,只要加入 NuGet System.Memory.dll 封裝的參考。

當然,請記住,那里可以將會中斷目前的預覽版本與功能實際上正在傳遞在穩定版本之間的變更。這類變更會大多是因為來自與您的開發人員的意見反應,嘗試使用的功能集。因此請試試看,並留意github.com/dotnet/coreclrgithub.com/dotnet/corefx儲存機制的進行中的工作。您也可以尋找文件,網址aka.ms/ref72

最後,此功能集成功依賴嘗試時提供意見反應,並建立他們自己使用這些類型,以提供有效且安全的存取權,記憶體的現代的.NET 程式為目標的所有的程式庫的開發人員。我們期待期待您有關您的經驗,以及更好,以進一步改善.NET GitHub 與您的工作。


作者: Stephen Toubmicrosoft.NET 上運作。您可以在 GitHub 上找到他github.com/stephentoub

這點受惠檢閱本文章下列技術專家:Krzysztof Cwalina、 Eric Erhardt、 Ahson 德、 Jan Kotas、 Jared Parsons、 Marek Safar、 夫拉迪 Sadov、 約瑟夫 Tremoulet、 帳單 Wagner、 Jan Vorlicek、 Karel Zikmund


MSDN Magazine 論壇中的這篇文章的討論