关于 Span 的一切:探索新的 .NET 明星: 2. Span<T> 是如何实现的?
2. Span<T> 是如何实现的?
内容列表:
- 1. Span<T> 是什么?
- 2. Span<T> 是如何实现的?
- 3. 什么是 Memory<T>,以及为什么你需要它?
- 4. Span
和 Memory 是如何与 .NET 库集成的? - 5. NET 运行时
- 6. C# 语言和编译器受到什么影响?
通常开发人员并不需要理解其使用的库是如何实现的。不过,对于 Span<T> 来说,它值得至少理解它后面的细节,因为这些细节影响到它的性能和使用方式的约束。
首先,Span<T> 是包含一个 ref 和长度的值类型,参考定义如下:
public readonly ref struct Span<T>
{
private readonly ref T _pointer;
private readonly int _length;
...
}
该 ref T
字段的概念在一开始可能奇怪 - 但是,在 C# 中并不能实际定义 ref T 字段,甚至在 MSIL 中。但是 Span<T> 实际上被开发用来使用运行时的特定内部类型,可以被看作 JIT,使用 JIT 生成等价的 ref T 字段。考虑更为常见的 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。该 Span<T> 与 ref T 是相同的思想,简单地在 struct 中封装。直接包含此类 ref 或者间接包含的类型被称为仿 ref 类型,C# 7.2 编译器使用 ref struct 签名来定义此类仿 ref 类型。
通过该总结,明确了两件事:
- 通过这种方式定义的 Span
,可以与数组一样高效:对 Span 的索引不需要从指针和其开始位置开始计算,因为 ref 字段已经封装了它们。( 相反,ArraySegment 有独立的 offset 字段,使得它在索引访问和分发时都变得更为昂贵 ) - Span
的本质是仿 ref 类型,带来了与 ref T 字段一样的约束。
第二个问题带来了有趣的分支,这些分支导致 .NET 包含由 Memory<T> 引导的第二中类型。