unsafe 和 fixed 是协同工作的底层机制:fixed 必须在 unsafe 上下文中使用,用于固定托管对象内存地址,防止 GC 移动;离开 fixed 块后指针立即失效,不可跨作用域传递或存储。

unsafe 和 fixed 不是“用法选择”,而是协同工作的底层机制:不加 fixed 的 unsafe 指针操作,多数情况下等于在赌 GC 不会移动对象——这在生产环境里基本等于主动引入随机崩溃。
为什么 fixed 必须和 unsafe 一起出现
fixed 语句只能出现在 unsafe 上下文中,编译器会直接拒绝在安全代码里写 fixed (int* p = arr)。这不是设计疏漏,而是语言强制的隔离层:一旦你决定绕过 CLR 的内存管理,就必须显式声明这个意图,并接受所有后果。
- 项目必须启用
AllowUnsafeBlocks(Visual Studio 中勾选“允许不安全代码”,或 MSBuild 中设<AllowUnsafeBlocks>true</AllowUnsafeBlocks>) -
fixed块内声明的指针是只读的,不能赋新地址,比如p = &otherVar会编译失败 - 离开
fixed块后,指针立即失效;继续使用(哪怕只是读(long)p)属于未定义行为,可能 crash,也可能看似正常但踩内存
fixed 能固定哪些东西:数组、字符串、字段、Span
不是所有变量都能被 fixed,它只接受能提供稳定内存地址的“可钉选”目标。常见合法类型包括:
-
int[]、byte[]等托管数组:固定后指针指向首元素,如fixed (byte* p = buffer) -
string:固定后得到char*,指向 UTF-16 首字符,注意不可写(字符串是只读的) - 类/结构体的字段:如
fixed (int* p = &obj.x),此时整个obj实例被钉住,防止 GC 移动它 -
Span<T>或ReadOnlySpan<T>:只要其底层是托管堆内存(比如来自array.AsSpan()),就支持fixed (T* p = span)
以下写法非法:
-
fixed (char* p = &str[0])——str[0]是值,不是变量,地址不可取 -
fixed (int* p = &localInt)—— 局部值类型本就在栈上,天然固定,无需fixed,编译器会报错 -
fixed (int* p = list.ToArray())——ToArray()返回新数组,但表达式求值后无引用持有,数组可能立刻被 GC 回收
fixed 的生命周期边界很窄,别跨作用域传指针
fixed 只保证“块内”对象不被移动。一旦控制流离开大括号,钉选解除,指针即作废。常见误用是把指针存到字段、返回给调用方、或塞进异步 lambda:
- ❌ 把
int*存进类字段:this.ptr = p—— 出fixed块后字段就悬空了 - ❌ 返回指针:
return p—— 编译器会阻止,但若绕过(如转成IntPtr)就彻底失控 - ❌ 在
Task.Run(() => { ... *p = 1; })里用p—— 线程调度+GC 时间不可控,大概率 crash
真正安全的做法是:所有指针计算、内存拷贝、位运算,都严格限制在 fixed { ... } 内完成。例如高速拷贝:
unsafe {
fixed (byte* src = source) fixed (byte* dst = destination) {
byte* s = src, d = dst;
for (int i = 0; i < length; i++) d[i] = s[i];
}
}替代方案比 fixed + unsafe 更常用
95% 的场景其实不需要 fixed。现代 .NET 提供了更安全、同样高效的替代路径:
- 用
Span<T>/Memory<T>做切片、复制、解析,零分配且自动处理钉选(内部调用GetPinnableReference()) - 用
stackalloc在栈上分配临时缓冲区(如int* buf = stackalloc int[256]),不涉及 GC,也无需fixed - 对字符串做 UTF-8 处理时,优先用
System.Text.UTF8Encoding.GetBytes(ReadOnlySpan<char>),而不是手动固定string
只有当你明确需要:① 直接与 C/C++ DLL 交互;② 实现极致性能的数值计算内核;③ 构建自定义序列化器并控制字节布局——才值得碰 fixed。其余时候,它只是个容易踩空的深坑。


















