在C#高性能编程场景里,Unsafe内存操作常被用来绕过托管堆的额外开销,直接读写连续内存。它允许使用指针、固定变量地址以及做底层字节转换,在图像处理和网络协议解析中非常实用。不过这种能力伴随着风险,理解其运行机制是写对代码的前提。

一、unsafe上下文与指针基础
要在C#中使用指针,必须将被标记的代码块或方法加上unsafe关键字,并在项目里允许不安全代码编译。指针本质是一个保存内存地址的变量,通过*解引用获取目标值,用&取地址。和C++不同,C#的指针只能指向值类型、数组或已固定的托管对象,不能指向普通引用类型对象内部字段。
下面示例展示最简单的指针用法,在栈上声明两个整数并通过指针修改:
using System;
class Program
{
unsafe static void Main()
{
int a = 10;
int b = 20;
int* pA = &a;
int* pB = &b;
*pA = *pA + *pB;
Console.WriteLine(a); // 输出 30
}
}
这段代码没有涉及托管堆,所有变量都在栈上,因此不需要fixed。但一旦指针指向堆上对象,就必须用fixed阻止垃圾回收器移动对象,否则地址失效会导致未定义行为。这也是新手最容易忽略的地方。
二、fixed语句与托管内存固定
当我们需要用指针遍历数组时,数组本身位于托管堆,GC在压缩阶段可能移动它。fixed语句会临时 pin 住对象,告诉GC这段时间别动这块内存,从而让指针稳定可用。固定是有成本的,它会增加GC碎片,因此不能长时间固定大对象。
以下例子用指针将数组每个元素乘二:
using System;
class Demo
{
unsafe static void DoubleArray(int[] arr)
{
fixed (int* p = arr)
{
for (int i = 0; i < arr.Length; i++)
{
p[i] = p[i] * 2;
}
}
}
static void Main()
{
int[] data = { 1, 2, 3 };
DoubleArray(data);
Console.WriteLine(string.Join(",", data)); // 2,4,6
}
}
fixed块结束后,数组解除固定,GC恢复自由。如果试图在fixed外部保存p指针,编译器会直接报错,这是安全护栏。在频繁调用的热点路径中,应尽量缩短fixed范围,或改用Span来避免指针。
三、Span与Unsafe的结合
Span<T>是安全的内存切片,它可以在栈上描述一段连续内存而不分配。通过System.Runtime.CompilerServices.Unsafe或MemoryMarshal,我们可以在Span和指针间转换,既保留安全API又获得底层速度。例如用Unsafe.As拿到结构体偏移,或用MemoryMarshal.GetReference获取首地址。
下面演示用MemoryMarshal将数组转为Span并用指针修改:
using System;
using System.Runtime.InteropServices;
class SpanDemo
{
static void Main()
{
int[] array = { 5, 6, 7 };
Span<int> span = array.AsSpan();
unsafe
{
ref int first = ref MemoryMarshal.GetReference(span);
fixed (int* p = &first)
{
p[1] = 99;
}
}
Console.WriteLine(array[1]); // 99
}
}
这种做法比纯指针代码更易维护,因为Span提供了边界检查。但在极高性能需求下,配合Unsafe跳过检查能再快一点。需要权衡可读性与速度,一般业务代码推荐Span,底层库才用裸指针。
四、常见误区与避坑建议
一个典型误区是认为unsafe等于自由内存管理。实际上C#不提供malloc和free,所有内存仍由CLR控制。若在unsafe里把托管对象的地址传给原生函数且未固定,函数回调时对象可能已被移动,造成崩溃。另一个误区是滥用stackalloc,它在栈上分配,过量会引发栈溢出。
推荐做法是用using var或短暂fixed,并优先选Span替代指针。下表对比两种方式:
| 方式 | 安全性 | 性能 | 适用场景 |
|---|---|---|---|
| 纯指针+fixed | 低,需手动保证 | 最高 | 底层库、极致优化 |
| Span+Unsafe | 中高,有边界检查 | 高 | 一般高性能代码 |
理解这些差异后,就能在项目中合理选用。记住,unsafe不是逃避管理的工具,而是精确控制内存的开关,用得好才安全高效。