新的C# Span<T>与ArraySegment<T>有何不同?

62

我在理解C#中新的Span<T>的用法方面遇到了困难。

  1. 它替代了哪些结构?ArraySegment<T>现在已经过时了吗?

  2. 它提供了哪些以前没有的功能?

  3. 在哪些情况下,Span<T>可以有效地替代C#数组?在哪些情况下不行?

  4. 什么时候应该使用ArraySegment<T>而不是Span<T>?

我正在尝试理解如何改变我的编码风格以有效地使用新的Span<T>。


ArraySegment 只适用于某些数据结构(如数组)的片段吗?Span 是否也受到相同限制? - mjwills
3
可能是与 什么是 ArraySegment<T> 类的用途? 相似的问题。 - mjwills
3个回答

77

Span<T>不是替代任何东西,而是增加了价值。它提供了一种类型安全的方式来查看连续的内存段,可以通过多种方式分配内存:可以分配为托管数组、基于堆栈的内存或非托管内存。

ArraySegment<T>仅限于托管数组。您无法使用它来包装使用stackalloc在堆栈上分配的数据。 Span<T>允许您这样做。

ArraySegment<T>也无法提供对底层数组的只读视图。 ReadOnlySpan<T>可以提供这个功能。

Span<T>并不是要替换数组。归根结底,它只是对数据的一个视图。这些数据必须以某种方式分配,在托管世界中,对于大多数情况,这种分配将是一个数组分配。因此,您仍然需要数组。

如果你想让你的代码能够操作不止数组,那么你应该使用Span<T>。例如,考虑一个解析库。现在,为了使其能够与数组、堆栈分配的内存和非托管内存一起使用,它必须为每个情况提供多个API入口点,并使用不安全的代码来实际操作数据。它可能还需要公开一个基于string的API,供那些将数据分配为字符串的人使用。有了SpanReadOnlySpan,您可以将所有这些逻辑合并到单个基于Span的解决方案中,在所有这些情况下都适用。

Span<T>并不是每个人都会经常使用的东西。它属于.NET框架中非常专业化的部分,主要对库作者和在高性能关键场景下有用。例如,ASP.NET Core后面的Web服务Kestrel将从使用Span<T>获得很多性能优势,因为例如可以使用Span<T>和栈分配的内存来解析请求,这不会对GC施加压力。但是,如果你编写基于ASP.NET Core的网站和服务,不一定需要使用它。


2
很好的解释 - 从您的描述中看来,Span<T>和ReadOnlySpan<T>相比ArraySegment<T>提供了更多的功能...那么我什么时候会使用ArraySegment<T>而不是Span<T>呢?因为看起来我永远不会用到它。 - TheFastCat
4
Span<T>是一个仅限于堆栈(stack only)的结构体,这是一种相当新且棘手的语言特性。你不能将其保存在某些非堆栈类或结构体的字段中。因此,Span<T>非常强大,但在使用上也相当有限制。另一方面,ArraySegment<T>非常简单且实用,每当你需要“缓冲区/偏移量/计数”三元组而Span<T>无法使用时,就可以使用它。 - Arek Bal
1
很酷,这就像是 Golang 中的 Slice。 - Bogdan Mart

18

来自MSDN杂志:Span的定义使得操作可以像数组一样高效: 对Span进行索引不需要计算出指针的开头和其起始偏移量,因为ref字段本身已经封装了这两个信息。(相比之下,ArraySegment有一个单独的偏移字段,这使得对其进行索引和传递都更加昂贵。)

另外,虽然ArraySegment实现了IEnumerable,但Span没有。


9
在决定是否使用 Span 时,还需考虑到 C# 中 ref structs 所适用的限制:
Span 是一种在堆栈上分配而非托管堆上的ref结构体。为确保其无法提升到托管堆,ref结构体类型有许多限制,包括不能装箱、不能赋值给Object、dynamic或任何接口类型的变量、不能成为引用类型中的字段,以及不能跨越await和yield边界使用。此外,调用两个方法Equals(Object)和GetHashCode会抛出NotSupportedException异常。 重要提示:
由于它是一个仅限于堆栈的类型,Span 不适用于许多需要存储对堆上缓冲区的引用的场景。例如,对于进行异步方法调用的例程来说是真实的。对于这样的情况,您可以使用补充的 System.Memory 和 System.ReadOnlyMemory 类型。
对于表示不可变或只读结构的跨度,请使用 System.ReadOnlySpan。
参考链接:https://learn.microsoft.com/en-us/dotnet/api/system.span-1?view=netcore-2.2https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref?view=netcore-2.2#ref-struct-types
将 ref 修饰符添加到结构声明中,定义该类型的实例必须在堆栈上分配。换句话说,这些类型的实例永远不能作为另一个类的成员在堆上创建。此功能的主要动机是 Span 和相关结构。
将 ref 结构类型保持为堆栈分配变量的目标引入了编译器强制执行的几个规则,适用于所有 ref 结构类型。
- 你不能装箱一个 ref 结构。 - 你不能将 ref 结构类型分配给 object、dynamic 或任何接口类型的变量。 - ref 结构类型无法实现接口。 - 你不能将 ref 结构声明为类或普通结构的成员。 - 你不能在异步方法中声明 ref 结构类型的本地变量。但可以在返回 Task、Task 或类似类型的同步方法中声明它们。 - 你不能在迭代器中声明 ref 结构本地变量。 - 你不能在 lambda 表达式或本地函数中捕获 ref 结构变量。 - 这些限制确保你不会意外地以可能将其提升到托管堆的方式使用 ref 结构。
你可以组合修饰符将结构声明为 readonly ref。readonly ref 结构结合了 ref 结构和 readonly 结构声明的优点和限制。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接