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

假设要公开特殊化排序例程,以就地对内存数据执行操作。可能要公开需要使用数组的方法,并提供对相应 T[] 执行操作的实现。如果方法的调用方有数组,且希望对整个数组进行排序,这样做就非常合适。但如果调用方只想对部分数组进行排序,该怎么办?可能还要公开需要使用偏移和计数的重载。但如果要支持的内存数据不在数组中,而是来自本机代码(举个例子)或位于堆栈上,并且你只有指针和长度,该怎么办?如何才能让编写的排序方法对内存的任意区域执行操作,同时还对完整数组或部分数组以及托管数组和非托管指针同样有效?

又例如,假设要对 System.String 实现操作,如使用特殊化分析方法。可能要公开需要使用字符串的方法,并提供对字符串执行操作的实现。但如果要支持对部分字符串执行操作,该怎么办?虽然 String.Substring 可用于分离出仅感兴趣的部分,但此操作的成本相对高昂,涉及字符串分配和内存复制。正如数组示例中提到的,可以使用偏移和计数。但如果调用方没有字符串,而是有 char[],该怎么办?或者,如果调用方有 char*(例如为了使用堆栈上某空间而使用 stackalloc 创建的,或通过调用本机代码而生成的),该怎么办?如果才能让编写的分析方法不强制调用方执行任何分配或复制操作,同时还对输入的类型字符串、char[] 和 char* 同样有效?

在这两个示例中,都可以使用不安全代码和指针,同时公开接受指针和长度的实现。不过,这样一来,就无法获取对 .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,以利用 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 IndexOutOfRangeExceptionbytes[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 spansbytes[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”返回类型进行声明,其中提供为数组编制索引的语义,同时返回对实际存储位置的引用,而不是相应位置上存在的副本:

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 allocationAssert.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>,对背后的运作机制详情至少有一个基本了解是值得的,因为这些详情暗含有关性能和使用约束的相关信息。

首先,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 编译器支持在签名中使用引用结构,从而声明这种类似引用的类型。

根据这一简要说明,应明确两点:

  1. Span<T> 的定义方式可确保操作效率与数组一样高:为 Span 编制索引无需通过计算来确定指针开头及其起始偏移,因为“引用”字段本身已对两者进行了封装。(相比之下,ArraySegment<T> 有单独的偏移字段,这就增加了索引编制和数据传递操作的成本。)

  2. 鉴于类似引用的类型这一本质,Span<T> 因其“引用 T”字段而受到一些约束。

第二点带来了一些有趣的后果,即导致 .NET 包含第二组相关的类型(由 Memory<T> 主导)。

什么是 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> 一样。

Span<T> 和 Memory<T> 如何与 .NET 库集成?

在上面的 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.Base64、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.Hash­Algorithm 上基于 Span<byte> 的新 TryComputeHash 方法显著减少分配操作量(每次算法迭代的字节数组,可能迭代数千次)和提升吞吐量。

这并未止步于核心 .NET 库一级,而是继续全面影响堆栈。ASP.NET Core 现在严重依赖 Span;例如,在 Span 基础之上编写 Kestrel 服务器的 HTTP 分析程序。Span 今后可能会通过较低级别 ASP.NET Core 中的公共 API 公开,如在它的中间件管道中。

.NET 运行时又如何呢?

.NET 运行时提供安全保障的方法之一是,确保为数组编制的索引不超出数组的长度,这种做法称为“边界检查”。例如,以下面这个方法为例:

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

在我撰写本文使用的 x64 计算机上,针对此方法生成的程序集如下所示:

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 指令将数据数组的长度与索引 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, edxadd      eax, dword ptr [rcx+4*r9+16]inc      edxcmp      r8d, edxjg       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, r8dadd      ecx, dword ptr [rax+4*r9]inc      r8dcmp      r8d, edxjl       SHORT G_M33812_IG03

程序集代码如此相似,部分是因为不用进行边界检查。此外,同样重要的是 JIT 将 Span 索引器识别为内部类型,即 JIT 为索引器生成特殊代码,而不是将它的实际 IL 代码转换为程序集。

所有这些都是为了说明运行时可以为 Span 应用与数组相同的优化类型,让 Span 成为高效的数据访问机制。如需了解更多详情,请参阅 bit.ly/2zywvyI 上的博客文章。

C# 语言和编译器又如何呢?

我已暗示,添加到 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。

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


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/321944.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#中DateTime的缺陷与代替品DateTimeOffset

C#中的DateTime在逻辑上有个非常严重的缺陷&#xff1a;> var d DateTime.Now;> var d2 d.ToUniversalTime();> d d2false> d.Equals(d2);false在C#交互模式中输入以上代码&#xff0c;可以发现尽管一个是本地时间&#xff08;d&#xff09;&#xff0c;一个是U…

重磅消息-Service Fabric 正式开源

微软的Azure Service Fabric的官方博客在2017.3.24日发布了一篇博客 Service Fabric .NET SDK goes open source &#xff0c;介绍了社区呼声最高的Service Fabric开源的情况以及当前的情况&#xff0c;当时开源了Service Fabric的.NET SDK部分&#xff0c;社区一直在期盼着Ser…

.NET Core 配置Configuration杂谈

前言.NET Core 在配置文件的操作上相对于.NET Framework做了不少改变&#xff0c;今天来聊一聊。关于Configuration的Package都是以Microsoft.Extensions.Configuration开头的支持多种方式的配置&#xff0c;包括内存、Json文件、XML文件等等&#xff0c;今天我们主要用Json格式…

秘密:从程序员到领导者的微妙之处

读到一篇关于程序员、领导力和领导者的好文章&#xff0c;翻译过来分享下。借用一句文中的话&#xff1a;“好吧&#xff0c;我只是个程序员&#xff0c;为什么需要领导&#xff1f;” &#xff0c;读完你就知道了。做一个优秀的领导者可能不是大部分程序员会立即认同的东西。许…

老衣的微服务实践简要指引2017版

这是老衣在2017年5月份总结的&#xff0c;适用于中小团队跨平台微服务开发的实践指引&#xff08;简化版&#xff09;。若有有不当之处&#xff0c;欢迎指点更正因本文涉及到大量第三方库或工具&#xff0c;详细学习和了解需要参考相关官方文档。若您在使用Mac电脑&#xff0c;…

Entity Framework Core 之数据库迁移

前言最近打算用.NET Core写一份开源的简易CMS系统,来练练手所以又去深入研究了一下Entity Framework Core 发现其实有些细节园子里还是很少讲到.特意整理了几个细节.正文1.数据库迁移先了解一下什么是"数据库迁移",它提供了一种方法&#xff0c;可以逐步将Code First…

随机挑战#4记录

正题 van♂van♂van♂成记录 题目 P2052−[NOI2011]P2052-[NOI2011]P2052−[NOI2011]道路修建【树】 博客链接:https://blog.csdn.net/Mr_wuyongcong/article/details/102249021 P2796−FacerP2796-FacerP2796−Facer的程序【dpdpdp】 博客链接:https://blog.csdn.net/Mr_wuy…

Visual Studio 2017 15.7 Preview 1 发布

上周 Visual Studio 2017 15.6 版和 Visual Studio for Mac 7.4 版已发布&#xff0c;今天 VS 发布了下一个次要更新的第一个预览&#xff1a;Visual Studio 2017 15.7。本预览的主要亮点包括&#xff1a;提高生产率更好的诊断额外的C 开发改进更好的Android和iOS环境管理通用 …

部署用于生产的Exceptionlees(一个强大易用的日志收集服务)

Exceptionless简介Exceptionless从翻译来看是无异常的意思&#xff0c;其实它收集了很多异常信息。大家可以把他看做事一个非常好用的日志收集服务。还提供了&#xff0c;多组织、多项目。它是一个开源项目&#xff0c;作者也非常的热心&#xff0c;项目地址https://github.com…

ASP.NET Core Web API下事件驱动型架构的实现(四):CQRS架构中聚合与聚合根的实现

在前面两篇文章中&#xff0c;我详细介绍了基本事件系统的实现&#xff0c;包括事件派发和订阅、通过事件处理器执行上下文来解决对象生命周期问题&#xff0c;以及一个基于RabbitMQ的事件总线的实现。接下来对于事件驱动型架构的讨论&#xff0c;就需要结合一个实际的架构案例…

如何看待微软新开源的Service Fabric?

本人曾在Service Fabric (SF)项目中做了几年&#xff0c;这次很兴奋能看到它的开源。短短两天已经有1300多个关注了。我看了一下开源的代码&#xff0c;真是满满的干货&#xff0c;200多万行C代码&#xff0c;所有最核心的协议算法都在里面。但是&#xff0c;不得不承认&#x…

RabbitMQ教程C#版 “Hello World”

先决条件 本教程假定RabbitMQ已经安装&#xff0c;并运行在localhost标准端口&#xff08;5672&#xff09;。如果你使用不同的主机、端口或证书&#xff0c;则需要调整连接设置。从哪里获得帮助 如果您在阅读本教程时遇到困难&#xff0c;可以通过邮件列表联系我们。1.介绍Rab…

牛客小白月赛18-记录

正题 比赛链接:https://ac.nowcoder.com/acm/contest/1221 成绩 总结 好难&#xff0c;就拿了一些水题分 T1:Forsaken喜欢数论\texttt{T1:Forsaken喜欢数论}T1:Forsaken喜欢数论 题目大意 f(i)f(i)f(i)表示iii的最小质因子&#xff0c;求∑i2nf(i)\sum_{i2}^nf(i)∑i2n​f(i…

.NET Core开源API网关 – Ocelot中文文档

Ocelot是一个用.NET Core实现并且开源的API网关&#xff0c;它功能强大&#xff0c;包括了&#xff1a;路由、请求聚合、服务发现、认证、鉴权、限流熔断、并内置了负载均衡器与Service Fabric、Butterfly Tracing集成。这些功能只都只需要简单的配置即可完成&#xff0c;下面我…

ABP前端使用阿里云angular2 UI框架NG-ZORRO分享

一、前言前段时间写博客分享和介绍了阿里云的UI框架NG-ZORRO&#xff08;博客请查看&#xff1a;http://www.cnblogs.com/donaldtdz/p/7892960.html&#xff09;&#xff0c;结合近段时间对.Net开源框架ABP的学习。完成将ABP前端框架替换成阿里云的NG-ZORRO。二、替换说明ABP版…

Prufer序列 生成树定理

Description 在图论中&#xff0c;树的定义是连通且无环的无向图。对于一棵有 nn 个节点且节点从 11 到 nn 编号的树&#xff0c;它的 Prufer 序列是一个唯一的长为 n−2n−2 的标号序列。 Prufer 序列的构造方法&#xff1a;每次删除树中标号最小的叶子节点&#xff08;即度为…

NOI.AC-积木【堆】

正题 题目链接:http://noi.ac/contest/266/problem/794 题目大意 无限多个1∗21*21∗2的砖块交替着 一个砖块会掉落仅当下方两个砖块都掉落&#xff0c;现在抽出nnn个砖块&#xff0c;求掉落多少个砖块。 解题思路 开一个优先队列&#xff0c;若两个连在一起的就把上面那个…

大规模开发团队如何实现DevOps转型? 来自微软全球开发平台工程团队的实践经验

微软全球开发平台工程团队从敏捷到DevOps的转型2013年11月13日&#xff0c;我们宣布了Visual Studio2013&#xff0c;以及微软研发云Visual Studio Online (VSO)的正式商用。紧接着我们经历了一次长达七小时的服务中断。我们的服务运行在一个“弹性扩展单元”中&#xff0c;为大…

野鸡NOI.AC模拟赛【2019.10.26】

前言 截止至2019.10.2614:222019.10.26\ \ \ \ 14:222019.10.26 14:22 成绩 正题 T1:NOI.AC−T1:NOI.AC-T1:NOI.AC−序列【堆】 https://blog.csdn.net/Mr_wuyongcong/article/details/102755906 T2:NOI.AC−T2:NOI.AC-T2:NOI.AC−积木【堆】 https://blog.csdn.net/Mr_wu…

Golang的CSP很酷?其实.NET也可以轻松完成

CSP&#xff08;Communicating sequential processes&#xff09;这东西我一开始以为很简单&#xff0c;后面差了资料发现它独树一帜&#xff0c;自己是一门语言&#xff0c;也是一套理论。这边我不深入的对它做过多的见解&#xff0c;我怕耽误大家_&#xff0c;大家可以看看wi…