[C#]丑陋的AWAIT语法:经历过一次AWAIT 函数调用后,`REF VAR` 定义的局部变量就找 先看代码public async ValueTaskError 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_id0, 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 referenceasync 状态机通常会被放到堆上因为它要在 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 是 ref struct也不能跨 awaitasync Task Foo() { Spanint span stackalloc int[10]; await Task.Delay(1000); span[0] 1; // 不允许 }原因类似Span 里面也包含对内存的 byref 引用它不能被提升到 async 状态机里。⸻怎么改方案一在 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 之后重新获取 refasync 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。

相关新闻

最新新闻

系统演进中的关键节点:从单机到微服务的架构决策指南

系统演进中的关键节点:从单机到微服务的架构决策指南

注意!系统演进中这些“重要节点”一旦错过,后面就要付出大代价 你有没有遇到过这样的场景:系统在测试环境一切正常,一上线就频繁超时;数据库 CPU 报警,DBA 凌晨三点打电话叫你起来看慢查询;每次…

2026/9/1 22:32:38
用Python验证Grok加州加德州比湾区更居中

用Python验证Grok加州加德州比湾区更居中

看到“Grok 定位加州加德州,比湾区更居中”这个标题,最容易产生两种反应:一种想讨论 Grok 的真实部署位置,另一种觉得“居中”根本说不清。这里不展开任何内部信息,只把这句话当成一道可复现的地理计算题:如…

2026/9/1 22:32:38
行空板K10+SIoTV2+Mind面板:轻量物联网图像监控实战

行空板K10+SIoTV2+Mind面板:轻量物联网图像监控实战

简介:本资源是一个面向物联网开发初学者与教育实践者的实时图像流监控系统完整实现方案,聚焦于行空板UNIHIKER K10摄像头的视频采集、高效传输及可视化集成,解决嵌入式端图像低延迟图传与跨平台监控展示的核心问题,适用于智能安防…

2026/9/1 22:32:38
西可韦X10挂耳式蓝牙耳机评测:舒适佩戴与性价比之选

西可韦X10挂耳式蓝牙耳机评测:舒适佩戴与性价比之选

这次我们来看一款在性价比和佩戴舒适度上引发热议的蓝牙耳机——西可韦 X10 挂耳式耳机。对于需要长时间佩戴耳机通勤、学习或轻度运动的朋友来说,挂耳式设计能否解决传统入耳式或头戴式的压迫感,同时保持稳定的连接和不错的音质,是大家最关心…

2026/9/1 22:32:38
小米2020校招软开笔试题复盘:核心考点与编程题解析

小米2020校招软开笔试题复盘:核心考点与编程题解析

先说明一点:手头这份标题是“小米2020校招软件开发工程师笔试题一”,并没有附带完整题目原文。所以这篇博文我是按当年小米软件研发岗校招笔试的真实风格,把最常出现的题型结构、典型考点、解题思路和现场踩坑经验做了完整复盘。如果你手上有…

2026/9/1 22:32:38
伴鱼2023秋招技术岗笔试E卷考点拆解与实战应对

伴鱼2023秋招技术岗笔试E卷考点拆解与实战应对

秋招笔试这一关,很多人以为拼的是刷题量,但我在看过不少真实笔试卷子之后发现,真正拉开差距的往往是读题速度和边界条件处理。伴鱼2023届秋招技术岗笔试E卷就是一个很典型的例子——它不考偏题怪题,但题目之间的梯度设计得很讲究&…

2026/9/1 22:27:38