[C#]丑陋的await语法:经历过一次await 函数调用后,`ref var` 定义的局部变量就找不到引用了
Posted on 2026-06-26 13:53 ahfuzhang 阅读(10) 评论(0) 收藏 举报作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!
先看代码:
public async ValueTask<Error> RunAsync()
{
ref readonly var req = ref this.Request;
ref var rsp = ref this.Response;
// ref var 不能跨越 async 方法中的 await 边界使用。
//
(DbConnection? db, Error err) = await DbPool.Pool!.GetAsync();
if (err.Err())
{
rsp.Code = 1;
rsp.Message = err.Message;
return default;
}
using var _ = db;
var (obj, err1) = await db!.ExecuteScalarAsync(
sql:"select count(1) from menus where menu_id>0",
parameters: null,
bindFunc: (MySqlCommand cmd) =>
{
return default;
}
);
if (err1.Err())
{
Response.Code = 2;
Response.Message = err1.Message;
return default;
}
Response.Message = obj?.ToString() ?? string.Empty;
// 如果按照下面这行代码的写法,就会抛异常
// rsp.Message = obj?.ToString() ?? string.Empty;
// 抛出异常:Object reference not set to an instance of an object.
return default;
}
我认为这个问题的根本还是编译器不够强大。
编译器完全有能力做到:ref var rsp 只是一个编译前的语法糖,而编译后,真正产生作用的是 Response.
跨 await 调用后, 居然就自动把 ref var 设置为 null,这实在太离谱了!如果某个分支我没有注意到,可能就会导致线上问题。
没办法了,只能以后自己小心。这个特性,成功把 C# 提升到 C++ 一样恶心的高度。
使用者必须自己了解这些缺陷,否则不小心就会出问题。
以下是来自 ChatGPT 的详细的规则解释:
提示词:请解释一下 C# 中 "ref var 不能跨越 async 方法中的 await 边界使用。" 这条原则
这条原则的核心是:
ref var / ref local / ref struct 这类“指向某块现有内存”的变量,不能在 await 之后继续使用,因为 await 会把方法拆成状态机,而这些“引用到栈上或对象内部位置”的东西不能安全地保存进状态机。
⸻
- 先看一个不允许的例子
async Task Foo()
{
int x = 10;
ref int r = ref x;
await Task.Delay(1000);
r = 20; // 不允许
}
编译器会报类似错误:
Cannot use ref local 'r' inside an anonymous method, lambda expression, query expression, or async method
或者类似“不能在 async 方法中跨 await 使用 ref local”的错误。
原因不是 r = 20 本身有问题,而是:
ref int r = ref x;
这里的 r 本质上不是一个普通变量,它是一个别名,直接指向 x 的存储位置。
⸻
- await 会把 async 方法拆成状态机
这个 async 方法:
async Task Foo()
{
int x = 10;
await Task.Delay(1000);
Console.WriteLine(x);
}
编译器大致会改写成一个状态机对象,类似:
class FooStateMachine
{
public int state;
public AsyncTaskMethodBuilder builder;
public int x;
public void MoveNext()
{
// await 前后的代码会被拆开执行
}
}
也就是说,await 前后的局部变量,如果后面还要用,通常会被“提升”到状态机字段里。
普通变量可以这么做:
int x;
可以变成:
public int x;
但是 ref int r 就麻烦了。
⸻
- 为什么 ref var 不能保存进状态机?
ref int r 表示:
r 是某个 int 存储位置的引用
它可能引用:
ref int r = ref array[0];
ref int r = ref span[0];
ref int r = ref someStruct.Field;
ref int r = ref localVariable;
这些引用有几个问题:
问题一:它可能指向栈上的变量
例如:
int x = 10;
ref int r = ref x;
如果 await 之后方法挂起,当前栈帧可能已经没了。r 如果还存在,就可能指向已经失效的栈内存。
C# 不允许这种悬空引用。
⸻
问题二:状态机是堆对象,不能安全保存 managed reference
async 状态机通常会被放到堆上,因为它要在 await 之后恢复执行。
但 ref int 这种东西不是普通对象引用,它是一个 managed pointer / byref。CLR 对这类引用有严格限制,不能随便作为对象字段长期保存。
否则 GC 移动对象、栈帧消失、对象生命周期变化时,都可能导致引用不安全。
⸻
问题三:ref 的生命周期必须非常短
ref 的设计目标是高性能、低开销地直接访问某块内存,但它的安全前提是:
编译器能清楚证明这个引用不会活得比被引用的东西更久。
await 会打断这个证明。
因为执行流程变成了:
执行 await 前代码
挂起方法
稍后恢复方法
继续执行 await 后代码
这中间变量的生命周期、栈帧、状态机位置都发生了变化。
⸻
- 正确理解:“不能跨 await”,不是“async 方法里完全不能用 ref var”
重点是不能跨越 await 边界使用。
比如这种通常是可以理解为安全模式的:
async Task Foo()
{
int[] arr = { 1, 2, 3 };
ref int r = ref arr[0];
r = 100;
await Task.Delay(1000);
Console.WriteLine(arr[0]);
}
这里 r 只在 await 之前使用,await 之后不用 r,逻辑上是安全的。
但是这种不行:
async Task Foo()
{
int[] arr = { 1, 2, 3 };
ref int r = ref arr[0];
await Task.Delay(1000);
r = 100; // 不允许
}
因为 r 活到了 await 之后。
⸻
- Span 也是类似问题
Span
async Task Foo()
{
Span<int> span = stackalloc int[10];
await Task.Delay(1000);
span[0] = 1; // 不允许
}
原因类似:Span
⸻
- 怎么改?
方案一:在 await 之前完成 ref 操作
async Task Foo()
{
int[] arr = { 1, 2, 3 };
ref int r = ref arr[0];
r = 100;
await Task.Delay(1000);
Console.WriteLine(arr[0]);
}
⸻
方案二:await 之后重新获取 ref
async Task Foo()
{
int[] arr = { 1, 2, 3 };
await Task.Delay(1000);
ref int r = ref arr[0];
r = 100;
}
也就是说,不要让 ref 变量本身跨过 await,但可以在 await 之后重新创建一个新的 ref。
⸻
方案三:保存索引,不保存 ref
错误思路:
ref int r = ref arr[index];
await SomethingAsync();
r = 100;
正确做法:
int savedIndex = index;
await SomethingAsync();
arr[savedIndex] = 100;
保存普通值,如 index、key、offset,而不是保存 ref。
⸻
方案四:把同步的 ref 操作拆出去
static void Modify(ref int value)
{
value = 100;
}
async Task Foo()
{
int[] arr = { 1, 2, 3 };
await Task.Delay(1000);
Modify(ref arr[0]);
}
这里 ref 只存在于同步方法调用期间,没有跨越 await。
⸻
- 本质总结
可以把这条规则理解成:
await 会把方法拆开,并把需要保留的局部变量搬到状态机里;
但 ref var 是“指向某个内存位置的临时引用”,不能被安全地搬到状态机里;
所以 ref var 不能活过 await。
一句话版:
ref var 是短生命周期的内存别名;await 会延长局部变量生命周期并改变执行上下文;两者不能混用跨边界。

浙公网安备 33010602011771号