Span
Span是 C# 7.2 引入的值类型,核心作用是零分配、高性能地表示连续的内存区域,可以直接操作数组、字符串、非托管内存等连续内存块,避免不必要的内存拷贝,大幅减少GC压力。
总的来说:Span就像一个 “内存视图”,不拥有内存,只是对已有内存的安全封装和高效访问。
1.1 核心用法
1.1.1 高性能内存操作,避免数组拷贝
传统方式处理数组子集时会产生拷贝,Span直接操作原内存:
1 2 3 4 5 6 7
| int[] source = { 1, 2, 3, 4, 5 }; int[] subArray = new int[3]; Array.Copy(source, 1, subArray, 0, 3);
Span<int> subSpan = source.AsSpan(1, 3);
|
1.1.2 字符串高效处理
字符串的Substring会创建新字符串(内存拷贝),而ReadOnlySpan.Slice无拷贝:
1 2 3 4 5 6 7 8
| string longText = "这是一段很长的文本,只需要截取前10个字符";
string subText = longText.Substring(0, 10);
ReadOnlySpan<char> subTextSpan = longText.AsSpan(0, 10); string result = subTextSpan.ToString();
|
1.1.3 替代不安全指针操作
Span在安全代码中实现了指针级别的性能,无需unsafe关键字:
1 2 3 4 5 6 7 8 9 10 11 12 13
| unsafe { int[] arr = { 1, 2, 3 }; fixed (int* p = arr) { p[1] = 10; } }
Span<int> arrSpan = new[] { 1, 2, 3 }; arrSpan[1] = 10;
|
1.2 注意事项
- Span是栈绑定的,不能用于异步方法的参数和返回值;不能用于类的字段;不能被闭包捕获。
- 只读场景用ReadOnlySpan例如字符串、不可变数据等,防止意外修改
- 仅用于连续内存,非连续不能用比如List
1.3补充
查看Span的定义可以发现它的结构体定义前面包含ref。ref struct是 C# 7.2 为配合Span这类高性能内存类型引入的特殊结构体类型,核心规则是:强制该结构体只能分配在栈上,绝对不能进入堆。普通结构体虽默认栈分配,但存在被 “装箱” 到堆的可能(比如赋值给object、接口,或作为类字段),而ref struct通过编译器强制禁止所有堆分配行为。
1.3.1 强制栈分配,杜绝内存安全问题
ref struct的设计初衷是为了安全地操作非托管内存 / 连续内存(比如Span需要指向数组、字符串或非托管内存的指针)。如果允许ref struct进入堆,会引发严重的内存安全问题:
- 栈内存的生命周期由栈帧(方法调用)决定,方法执行完栈帧销毁,栈内存就释放;
- 堆内存的生命周期由 GC 决定,若ref struct(含内存指针)被放到堆上,其指向的栈内存可能已释放,导致悬垂引用(类似 C++ 野指针)。
通过ref struct强制栈绑定,确保:
- ref struct的生命周期 ≤ 其引用的内存生命周期
- 编译器直接阻断所有可能将ref struct放到堆的操作(比如赋值给类字段、异步方法参数等)。
总之ref struct的核心作用即:强制栈分配 + 允许安全持有 ref / 指针字段,杜绝因堆分配导致的内存悬垂、野指针等安全问题
1.3.2 ref T和T*
这俩都是指向内存地址,可以直接操作内存,但绝对不是一个东西。
ref T是C#提供的安全托管的内存引用,而T*是非托管的指针,需要unsafe上下文,不受CLR安全管控
- 在发生GC Compact时,
ref T回跟踪对象挪动位置自动调整引用地址,而T*可能会导致悬垂引用
ref T提供强类型安全,不能随意转换,而T*弱类型安全,可以强制转换为任意指针。ref T编译器提供引用有效性,例如栈绑定,T*无约束
1.4 与stackalloc配合
stackalloc是 C# 关键字,用于在栈上分配连续的内存块(而非堆),分配的内存会随方法栈帧销毁自动释放(无需 GC 回收),极致高性能,但受栈大小限制(默认进程栈大小约 1MB),仅适合小内存块分配。
1 2
| Span<T> span = stackalloc T[size];
|
在C# 7.2 前,stackalloc需要配合非托管指针使用(unsafe块)
1 2 3 4 5
| unsafe { int* p = stackalloc int[16]; }
|
1.5 一个小应用
如何通过索引器像数组一样访问结构体内部的字段?下面提供了三种方式
1.5.1 指针写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| [StructLayout(LayoutKind.Sequential)] public unsafe struct Data { public int A; public int B; public int C;
public int this[int i] { get { if ((uint)i >= 3) throw new IndexOutOfRangeException();
fixed (int* p = &A) { return p[i]; } } } }
|
1.5.2 MemoryMarshal + span
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)] public struct Data { public int A; public int B; public int C;
public int this[int i] { get { if ((uint)i >= 3) throw new IndexOutOfRangeException();
Span<int> span = MemoryMarshal.CreateSpan(ref A, 3); return span[i]; } } }
|
1.5.3 Unsafe.Add
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| [StructLayout(LayoutKind.Sequential)] public struct Data { public int A; public int B; public int C;
public int this[int i] { get { if ((uint)i >= 3) throw new IndexOutOfRangeException();
return Unsafe.Add(ref A, i); } } }
|