假设要公开特殊化排序例程,以就地对内存数据执行操做。可能要公开须要使用数组的方法,并提供对相应 T[] 执行操做的实现。若是方法的调用方有数组,且但愿对整个数组进行排序,这样作就很是合适。但若是调用方只想对部分数组进行排序,该怎么办?可能还要公开须要使用偏移和计数的重载。但若是要支持的内存数据不在数组中,而是来自本机代码(举个例子)或位于堆栈上,而且你只有指针和长度,该怎么办?如何才能让编写的排序方法对内存的任意区域执行操做,同时还对完整数组或部分数组以及托管数组和非托管指针一样有效?git
又例如,假设要对 System.String 实现操做,如使用特殊化分析方法。可能要公开须要使用字符串的方法,并提供对字符串执行操做的实现。但若是要支持对部分字符串执行操做,该怎么办?虽然 String.Substring 可用于分离出仅感兴趣的部分,但此操做的成本相对高昂,涉及字符串分配和内存复制。正如数组示例中提到的,可使用偏移和计数。但若是调用方没有字符串,而是有 char[],该怎么办?或者,若是调用方有 char*(例如为了使用堆栈上某空间而使用 stackalloc 建立的,或经过调用本机代码而生成的),该怎么办?若是才能让编写的分析方法不强制调用方执行任何分配或复制操做,同时还对输入的类型字符串、char[] 和 char* 一样有效?github
在这两个示例中,均可以使用不安全代码和指针,同时公开接受指针和长度的实现。不过,这样一来,就没法获取对 .NET 相当重要的安全保障,而且会遇到对大多数 .NET 开发人员而言已成为过去的问题,如缓冲区溢出和访问冲突。此外,这还会引起其余性能损失,如须要在操做期间固定托管对象,让检索的指针一直有效。并且根据涉及的数据类型,获取指针根本就不可行。算法
此难题仍是有解决方法的,即便用 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,以利用 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
更为广泛的是,Span 可用于引用任意指针和长度(如经过本机堆分配的内存),以下所示:框架
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# 语言功能,即引用返回。索引器使用“引用 T”返回类型进行声明,其中提供为数组编制索引的语义,同时返回对实际存储位置的引用,而不是相应位置上存在的副本:dom
public ref T this[int index] { get { ... } }
经过示例,能够最明显地体现这种引用返回类型索引器带来的影响,如将它与不是引用返回类型的 List<T> 索引器进行比较。例如:
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 功能来返回“引用只读 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
Span 的优点还有许多,远不止已提到的这些。例如,Span 支持 reinterpret_cast 的理念,便可以将 Span<byte> 强制转换为 Span<int>(其中,Span<int> 中的索引 0 映射到 Span<byte> 的前四个字节)。这样一来,若是读取字节缓冲区,能够安全高效地将它传递到对分组字节(视做整数)执行操做的方法。
开发人员一般无需了解要使用的库是如何实现的。不过,对于 Span<T>,对背后的运做机制详情至少有一个基本了解是值得的,由于这些详情暗含有关性能和使用约束的相关信息。
首先,Span<T> 是包含引用和长度的值类型,定义大体以下:
public readonly ref struct Span<T> { private readonly ref T _pointer; private readonly int _length; ... }
“引用 T”字段这一律念初看起来有些奇怪,由于其实没法在 C# 或甚至 MSIL 中声明“引用 T”字段。不过,Span<T> 实际上旨在于运行时使用特殊内部类型,可看做是内部实时 (JIT) 类型,由 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]);
此代码经过引用传递数组中的槽,这样(除优化外)还能够在堆栈上生成引用 T。Span<T> 中的引用 T 有殊途同归之妙,直接封装在结构中。直接或间接包含此类引用的类型被称为相似引用的类型,C# 7.2 编译器支持在签名中使用引用结构,从而声明这种相似引用的类型。
根据这一简要说明,应明确两点:
第二点带来了一些有趣的后果,即致使 .NET 包含第二组相关的类型(由 Memory<T> 主导)。
Span<T> 是相似引用的类型,由于它包含“引用”字段,并且“引用”字段不只能够引用数组等对象的开头,还能够引用它们的中间部分:
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 运行时的垃圾回收器,跟踪这些指针是一项成本相对高昂的操做。所以,运行时将这些引用约束为仅存在于堆栈上,由于它隐式规定了能够存在的内部指针数量下限。
此外,如前所述,Span<T> 大于计算机的字大小;也就是说,对 Span 执行的读取和写入操做不是原子操做。若是多个线程同时对 Span 在堆上的字段执行读取和写入操做,存在“撕裂”风险。 假设现有一个已初始化的 Span,其中包含有效引用和值为 50 的相应 _length。一个线程开始编写新 Span,而且还编写新 _pointer 值。而后,还未将相应的 _length 设置为 20,另外一个线程就开始读取 Span,其中包含新 _pointer 和更长的旧 _length。
这样一来,Span<T> 示例只能存在于堆栈上,而不能存在于堆上。也就是说,没法将 Span 装箱,进而没法将 Span<T> 与现有反射调用 API(举个例子)结合使用,由于它们须要执行装箱。这意味着,没法将 Span<T> 字段封装在类中,甚至也没法封装在不相似引用的结构中。也就是说,若是 Span 可能会隐式成为类中的字段,则没法使用它们。例如,将它们捕获到 lambda 中,或将它们捕获为异步方法或迭代器中的本地字段,由于这些本地字段可能最终会成为编译器生成的状态机上的字段。 这还意味着,没法将 Span<T> 用做泛型参数,由于类型参数实例可能最终会被装箱或以其余方式存储到堆上(暂无“where T : ref struct”约束)。
对于许多方案,尤为是对于受计算量限制和同步处理功能,这些限制可有可无。不过,异步功能倒是另外一回事。不管是处理同步操做仍是异步操做,本文开头提到的大部分有关数组、数组切片和本机内存等问题仍存在。但若是 Span<T> 没法存储到堆,于是没法跨异步操做暂留,那么还有什么解决方法?答案就是 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; ... }
能够经过数组建立 Memory<T>,并进行切片。这与处理 Span 基本相同,不一样之处在于 Memory<T> 是不相似引用的结构,能够存在于堆上。而后,若要执行同步处理,能够从中获取 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> 也有等效的只读类型,即 ReadOnlyMemory<T>。与预期同样,它的 Span 属性返回 ReadOnlySpan<T>。请参阅图 1,快速概览在这些类型之间进行转换的内置机制。
图 1:在 Span 相关类型之间进行非分配/非复制转换
来自 | 收件人 | 机制 |
ArraySegment<T> | Memory<T> | 隐式强制转换、AsMemory 方法 |
ArraySegment<T> | ReadOnlyMemory<T> | 隐式强制转换、AsReadOnlyMemory 方法 |
ArraySegment<T> | ReadOnlySpan<T> | 隐式强制转换、AsReadOnlySpan 方法 |
ArraySegment<T> | Span<T> | 隐式强制转换、AsSpan 方法 |
ArraySegment<T> | T[] | Array 属性 |
Memory<T> | ArraySegment<T> | TryGetArray 方法 |
Memory<T> | ReadOnlyMemory<T> | 隐式强制转换、AsReadOnlyMemory 方法 |
Memory<T> | Span<T> | Span 属性 |
ReadOnlyMemory<T> | ArraySegment<T> | DangerousTryGetArray 方法 |
ReadOnlyMemory<T> | ReadOnlySpan<T> | Span 属性 |
ReadOnlySpan<T> | ref readonly T | 索引器 get 取值函数、封送处理方法 |
Span<T> | ReadOnlySpan<T> | 隐式强制转换、AsReadOnlySpan 方法 |
Span<T> | ref T | 索引器 get 取值函数、封送处理方法 |
字符串 | ReadOnlyMemory<char> | AsReadOnlyMemory 方法 |
字符串 | ReadOnlySpan<char> | 隐式强制转换、AsReadOnlySpan 方法 |
T[] | ArraySegment<T> | 构造函数、隐式强制转换 |
T[] | Memory<T> | 构造函数、隐式强制转换、AsMemory 方法 |
T[] | ReadOnlyMemory<T> | 构造函数、隐式强制转换、AsReadOnlyMemory 方法 |
T[] | ReadOnlySpan<T> | 构造函数、隐式强制转换、AsReadOnlySpan 方法 |
T[] | Span<T> | 构造函数、隐式强制转换、AsSpan 方法 |
void* | ReadOnlySpan<T> | 构造函数 |
void* | Span<T> | 构造函数 |
将会注意到,Memory<T> 的 _object 字段并未强类型化为 T[],而是存储为对象。这突出说明 Memory<T> 能够包装数组之外的内容,如 System.Buffers.OwnedMemory<T>。OwnedMemory<T> 是抽象类,可用于包装须要密切管理其生存期的数据,如从池中检索到的内存。此主题更为高级,超出了本文的介绍范围,但这就是 Memory<T> 的用途所在(例如,用于将指针包装到本机内存)。ReadOnlyMemory<char> 也能够与字符串结合使用,就像 ReadOnlySpan<char> 同样。
在上面的 Memory<T> 代码片断中,将会注意到传入 Memory<byte> 的 Stream.ReadAsync 调用。但现在在 .NET 中,Stream.ReadAsync 被定义为接受 byte[]。它的工做原理是什么?
为了支持 Span<T> 及其成员,即将向 .NET 添加数百个新成员和类型。其中大可能是现有基于数组和基于字符串的方法的重载,而另外一些则是专一于特定处理方面的全新类型。例如,除了包含须要使用字符串的现有重载外,全部原始类型(如 Int32)如今都包含接受 ReadOnlySpan<char> 的 Parse 重载。假设字符串包含两部分数字(用逗号隔开,如“123,456”),且但愿分析这部分数字。如今,能够编写以下代码:
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));
经过使用基于 Span 的新 Parse 重载,能够在这整个操做期间避免执行分配操做。相似分析和格式化方法可用于原始类型(如 Int32),其中包括 DateTime、TimeSpan 和 Guid 等核心类型,甚至还包括 BigInteger 和 IPAddress 等更高级别类型。
实际上,已跨框架添加了许多这样的方法。从 System.Random 到 System.Text.StringBuilder,再到 System.Net.Socket,这些重载的添加有利于轻松高效地处理 {ReadOnly}Span<T> 和 {ReadOnly}Memory<T>。其中一些甚至带来了额外的好处。例如,Stream 现包含如下方法:
public virtual ValueTask<int> ReadAsync( Memory<byte> destination, CancellationToken cancellationToken = default) { ... }
将会注意到,不一样于接受 byte[] 并返回 Task<int> 的现有 ReadAsync 方法,此重载不只接受 Memory<byte>(而不是 byte[]),还返回 ValueTask<int>(而不是 Task<int>)。在如下状况下,ValueTask<T> 是有助于避免执行分配操做的结构:常常要求使用异步方法来同步返回内容,以及不太可能为全部常见返回值缓存已完成任务。例如,运行时能够为结果 true 和 false 缓存已完成的 Task<bool>,但没法为 Task<int> 的全部可能结果值缓存四十亿任务对象。
因为至关常见的是 Stream 实现的缓冲方式让 ReadAsync 调用同步完成,所以这一新 ReadAsync 重载返回 ValueTask<int>。也就是说,同步完成的异步 Stream 读取操做能够彻底避免执行分配操做。ValueTask<T> 也用于其余新重载,如 Socket.ReceiveAsync、Socket.SendAsync、WebSocket.ReceiveAsync 和 TextReader.ReadAsync 重载。
此外,在一些状况下,Span<T> 还支持向框架添加在过去引起内存安全问题的方法。假设要建立的字符串包含随机生成的值(如某类 ID)。如今,可能会编写要求分配字符数组的代码,以下所示:
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>,这样就无需使用不安全代码。此方法还利用接受 ReadOnlySpan<char> 的新字符串构造函数,以下所示:
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 个字节),可使用此方法;但若是长度为数千字节,很容易就会引起堆栈溢出问题。若是能够改成直接写入字符串的内存,该怎么办?Span<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);
实现此方法是为了分配字符串,并分发可写 Span,执行写入操做后能够在构造字符串的同时填写字符串的内容。请注意,在此示例中,Span<T> 的仅限堆栈这一本质很是有用,由于能够保证在字符串的构造函数完成前 Span(引用字符串的内部存储)就不存在,这样便没法在构造完成后使用 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'); } });
如今,不只避免了分配操做,还能够直接写入字符串在堆上的内存,即也避免了复制操做,且不受堆栈大小限制的约束。
除了核心框架类型有新成员外,咱们还正在积极开发许多可与 Span 结合使用的新 .NET 类型,从而在特定方案中实现高效处理。例如,对于要编写高性能微服务和处理大量文本的网站的开发人员,若是在使用 UTF-8 时无需编码和解码字符串,则性能会大大提高。为此,咱们即将添加 System.Buffers.Text.Base6四、System.Buffers.Text.Utf8Parser 和 System.Buffers.Text.Utf8Formatter 等新类型。这些类型对字节 Span 执行操做,不只避免了 Unicode 编码和解码,还可以处理在各类网络堆栈的最低级别中常见的本机缓冲:
ReadOnlySpan<byte> utf8Text = ...; if (!Utf8Parser.TryParse(utf8Text, out Guid value, out int bytesConsumed, standardFormat = 'P')) throw new InvalidDataException();
全部此类功能不只仅只用于公共使用用途;框架自己也能够利用这些基于 Span<T> 和基于 Memory<T> 的新方法来提高性能。跨 .NET Core 调用网站已切换为使用新的 ReadAsync 重载,以免没必要要的分配操做。分析过去是经过分配子字符串完成,如今能够避免执行分配操做。甚至 Rfc2898DeriveBytes 等间隙类型也实际运用了此功能,利用 System.Security.Cryptography.HashAlgorithm 上基于 Span<byte> 的新 TryComputeHash 方法显著减小分配操做量(每次算法迭代的字节数组,可能迭代数千次)和提高吞吐量。
这并未止步于核心 .NET 库一级,而是继续全面影响堆栈。ASP.NET Core 如今严重依赖 Span;例如,在 Span 基础之上编写 Kestrel 服务器的 HTTP 分析程序。Span 从此可能会经过较低级别 ASP.NET Core 中的公共 API 公开,如在它的中间件管道中。
.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 进行比较。若是 3 超出范围(异常抛出),后续 jbe 指令会转到范围检查失败例程。虽然 JIT 须要生成代码,以确保此类访问不会超出数组边界,但这并不意味着每一个数组访问都须要进行边界检查。如下面的 Sum 方法为例:
static int Sum(int[] data) { int sum = 0; for (int i = 0; i < data.Length; i++) sum += data[i]; return sum; }
虽然 JIT 此时须要生成代码,以确保对 data[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 指令仍在循环中,但只需将 i 值(存储在 edx 寄存器中)与数组长度(存储在 r8d 寄存器中)进行比较,无需额外进行边界检查。
运行时向 Span(Span<T> 和 ReadOnlySpan<T>)应用相似优化。将上面的示例与下面的代码进行比较,惟一的变化是参数类型:
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 将 Span 索引器识别为内部类型,即 JIT 为索引器生成特殊代码,而不是将它的实际 IL 代码转换为程序集。
全部这些都是为了说明运行时能够为 Span 应用与数组相同的优化类型,让 Span 成为高效的数据访问机制。如需了解更多详情,请参阅 bit.ly/2zywvyI 上的博客文章。
我已暗示,添加到 C# 语言和编译器的功能有助于让 Span<T> 成为 .NET 中的一流成员。C# 7.2 的多项功能都与 Span 相关(实际上,C# 7.2 编译器必须使用 Span<T>)。接下来,将介绍三个此类功能。
引用结构。如前所述,Span<T> 是相似引用的类型,自版本 7.2 起在 C# 中公开为引用结构。经过将引用关键字置于结构前,能够指示 C# 编译器将其余引用结构类型(如 Span<T>)用做字段,这样作还会注册要分配给类型的相关约束。例如,若要为 Span<T> 编写结构枚举器,枚举器须要存储 Span<T>,所以它自己必须是引用结构,以下所示:
public ref struct Enumerator { private readonly Span<char> _span; private int _index; ... }
Span 的 stackalloc 初始化。在旧版 C# 中,只能将 stackalloc 的结果存储到指针本地变量中。自 C# 7.2 起,如今能够在表达式中使用 stackalloc,并能定目标到 Span,而不使用不安全关键字。由于,无需编写:
Span<byte> bytes; unsafe { byte* tmp = stackalloc byte[length]; bytes = new Span<byte>(tmp, length); }
只需编写:
Span<byte> bytes = stackalloc byte[length];
若是须要一些空间来执行操做,但又但愿避免分配相对较小的堆内存,此代码就很是有用。过去有如下两种选择:
如今,不使用代码复制,便可完成相同的操做,并且还可使用安全代码和最简单的操做:
Span<byte> bytes = length <= 128 ? stackalloc byte[length] : new byte[length]; ... // Code that operates on the Span<byte>
Span 使用验证。由于 Span 能够引用可能与给定堆栈帧相关联的数据,因此传递 Span 可能存在危险,此操做可能会引用再也不有效的内存。例如,假设方法尝试执行如下操做:
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# 编译器使用引用结构检测此类无效使用,并会中止编译,同时显示如下错误:
错误 CS8352:没法在此上下文中使用本地“字符”,由于它可能会在声明范围外公开引用的变量
本文介绍的类型、方法、运行时优化和其余元素即将顺利添加到 .NET Core 2.1 中。以后,我预计它们会全面影响 .NET Framework。核心类型(如 Span<T>)和新类型(如 Utf8Parser)也即将顺利添加到与 .NET Standard 1.1 兼容的 System.Memory.dll 包中。这样一来,相关功能将适用于现有 .NET Framework 和 .NET Core 版本,尽管在内置于平台时没有实现一些优化。如今,能够试用此包的预览版,只需添加对 NuGet 上 System.Memory.dll 包的引用便可。
固然,请注意,当前预览版与实际发布的稳定版之间可能会有重大变革。此类变革很大程度上源于像你这样的开发人员在试用功能集时提供的反馈。所以,请试用预览版,并关注 github.com/dotnet/coreclr 和 github.com/dotnet/corefx 存储库,以掌握最新动态。此外,有关文档,还能够访问 aka.ms/ref72。
总的来讲,此功能集可否取得成功依赖开发人员试用预览版、提供反馈以及利用这些类型生成本身的库,全部这些都是为了可以在新式 .NET 程序中高效安全地访问内存。咱们热切期待聆听你们的使用体验反馈,最好可以与你们一块儿在 GitHub 上进一步改进 .NET。
Stephen Toub 就任于 Microsoft,负责 .NET 产品。能够在 GitHub (github.com/stephentoub) 上关注他。
本文转自 https://msdn.microsoft.com/zh-cn/magazine/mt814808