原文:
译注:这是本系列最后一篇文章
背景
.net是一个托管平台,这意味着内存访问和管理是安全的、自动的。所有类型都是由.net完全管理的,它在执行栈或托管堆上分配内存。
在互操作的事件或低级别开发中,你可能希望访问本机对象和系统内存,这就是为什么会有互操作这部分了,有一部分类型可以封送进入本机世界,调用本机api,转换托管/本机类型和在托管代码中定义一个本机结构。
问题1:内存访问模式
在.net世界中,你可能会对3种内存类型感兴趣。
上面每种类型的内存访问可能需要使用为它设计的语言特性:
你看,不同的访问模式需要不同的代码,对于所有连续的内存访问没有单一的内置类型。
问题2:性能
在许多应用程序中,最消耗cpu的操作是字符串操作。如果你对你的应用程序运行一个分析器会话,你可能会发现95%的cpu时间都用于调用字符串和相关函数。
trim、isnullorwhitespace和substring可能是最常用的字符串api,它们也很重:
span<t>
system.span<t>是一个只在栈上的类型(ref struct),它封装了所有的内存访问模式,它是一种用于通用连续内存访问的类型。你可以认为span<t>的实现包含一个虚拟引用和一个长度,接受全部3种内存访问类型。
你可以使用span<t>的构造函数重载或来自数组、stackalloc的指针和非托管指针的隐式操作符来创建span<t>。
// 使用隐式操作 span<char>(char[])。 span<char> span1 = new char[] { 's', 'p', 'a', 'n' }; // 使用stackalloc。 span<byte> span2 = stackalloc byte[50]; // 使用构造函数。 intptr array = new intptr(); span<int> span3 = new span<int>(array.topointer(), 1);
一旦你有了一个span<t>对象,你可以用指定的索引来设置值,或者返回span的一部分:
// 创建一个实例: span<char> span = new char[] { 's', 'p', 'a', 'n' }; // 访问第一个元素的引用。 ref char first = ref span[0]; // 给引用设置一个新的值。 first = 's'; // 新的字符串"span". console.writeline(span.toarray());
// 返回一个新的span从索引1到末尾. // 得到"pan"。 span<char> span2 = span.slice(1); console.writeline(span2.toarray());
你可以使用slice()方法编写一个高性能trim()方法:
private static void main(string[] args) { string test = " hello, world! "; console.writeline(trim(test.tochararray()).toarray()); } private static span<char> trim(span<char> source) { if (source.isempty) { return source; } int start = 0, end = source.length - 1; char startchar = source[start], endchar = source[end]; while ((start < end) && (startchar == ' ' || endchar == ' ')) { if (startchar == ' ') { start++; } if (endchar == ' ') { end—; } startchar = source[start]; endchar = source[end]; } return source.slice(start, end - start + 1); }
上面的代码不复制字符串,也不生成新的字符串,它通过调用slice()方法返回原始字符串的一部分。
因为span<t>是一个ref结构,所以所有的ref结构限制都适用。也就是说,你不能在字段、属性、迭代器和异步方法中使用span<t>。
memory<t>
system.memory<t>是一个system.span<t>的包装。使其在迭代器和异步方法中可访问。使用memory<t>上的span属性来访问底层内存,这在异步场景中非常有用,比如文件流和网络通信(httpclient等)。
下面的代码展示了这种类型的简单用法。
private static async task main(string[] args) { memory<byte> memory = new memory<byte>(new byte[50]); int count = await readfromurlasync("https://www.microsoft.com", memory).configureawait(false); console.writeline("bytes written: {0}", count); } private static async valuetask<int> readfromurlasync(string url, memory<byte> memory) { using (httpclient client = new httpclient()) { stream stream = await client.getstreamasync(new uri(url)).configureawait(false); return await stream.readasync(memory).configureawait(false); } }
框架类库/核心框架(fcl/corefx)将在.net core 2.1中为流、字符串等添加基于类span类型的api。
readonlyspan<t> 和 readonlymemory<t>
system.readonlyspan<t>是system.span<t>的只读版本。其中,索引器返回一个只读的ref对象,而不是ref对象。在使用system.readonlyspan<t>这个只读的ref结构时,你可以获得只读的内存访问权限。
这对于string类型非常有用,因为string是不可变的,所以它被视为只读的span。
我们可以重写上面的代码来实现trim()方法,使用readonlyspan<t>:
private static void main(string[] args) { // implicit operator readonlyspan(string). readonlyspan<char> test = " hello, world! "; console.writeline(trim(test).toarray()); } private static readonlyspan<char> trim(readonlyspan<char> source) { if (source.isempty) { return source; } int start = 0, end = source.length - 1; char startchar = source[start], endchar = source[end]; while ((start < end) && (startchar == ' ' || endchar == ' ')) { if (startchar == ' ') { start++; } if (endchar == ' ') { end—; } startchar = source[start]; endchar = source[end]; } return source.slice(start, end - start + 1); }
如你所见,方法体中没有任何更改;我只是将参数类型从span<t>更改为readonlyspan<t>,并使用隐式操作符将字符串直接转换为readonlyspan<char>。
memory扩展方法
system.memoryextensions类包含针对不同类型的扩展方法,这些方法使用span类型进行操作,下面是常用的扩展方法列表,其中许多是使用span类型的现有api的等效实现。
内存封送
在某些情况下,你可能希望对内存类型和系统缓冲区有较低级别的访问权限,并在span和只读span之间进行转换。system.runtime.interopservices.memorymarshal静态类提供了此类功能,允许你控制这些访问场景。下面的代码展示了使用span类型来做首字母大写,这个实现性能高,因为没有临时的字符串分配。
private static void main(string[] args) { string source = "span like types are awesome!"; // source.tomemory() 转换变量 source 从字符串类型为 readonlymemory<char>, // and memorymarshal.asmemory 转换 readonlymemory<char> 为 memory<char> // 这样你就可以修改元素了。 titlecase(memorymarshal.asmemory(source.asmemory())); // 得到 "span like types are awesome!"; console.writeline(source); } private static void titlecase(memory<char> memory) { if (memory.isempty) { return; } ref char first = ref memory.span[0]; if (first >= 'a' && first <= 'z') { first = (char)(first - 32); } }
结论
span<t>和memory<t>支持以统一的方式访问连续内存,而不管内存是如何分配的。它对本地开发场景以及高性能场景非常有帮助。特别是,在使用span类型处理字符串时,你将获得显著的性能改进。这是c# 7.2中一个非常好的创新特性。
注意:要使用此功能,你需要使用visual studio 2017.5和c#语言版本7.2或最新版本。
系列文章:
如对本文有疑问, 点击进行留言回复!!
使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)
C#实现获取本地内网(局域网)和外网(公网)IP地址的方法分析
网友评论