[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。

相关新闻

最新新闻

应用安全加固实战:从算法分析到代码混淆的逆向工程对抗

应用安全加固实战:从算法分析到代码混淆的逆向工程对抗

1. 项目概述:从“黑盒”到“白盒”的安全攻防思维转变做安全开发或者逆向分析的朋友,对上面这个标题里的关键词应该都不陌生。这听起来像是一节安全课程的目录,但它背后指向的是一个非常核心且实战性极强的领域:应用安全加固与逆向…

2026/7/6 23:16:04
移远EC200S Linux 5.4+ 驱动移植:3处内核源码修改与PPP拨号成功率提升

移远EC200S Linux 5.4+ 驱动移植:3处内核源码修改与PPP拨号成功率提升

Linux 5.4内核下EC200S驱动移植与PPP拨号优化实战指南在嵌入式Linux系统中集成4G通信模块已成为工业物联网、智能终端等领域的标配方案。移远EC200S作为一款高性价比的LTE Cat 1模块,其稳定性和低功耗特性备受开发者青睐。本文将深入探讨在新版Linux内核&#xff08…

2026/7/6 23:16:04
锂离子电池BMS设计核心:3大关键算法与充放电安全防护策略详解

锂离子电池BMS设计核心:3大关键算法与充放电安全防护策略详解

锂离子电池BMS设计核心:3大关键算法与充放电安全防护策略详解1. BMS算法架构与电池安全的内在逻辑现代锂离子电池管理系统(BMS)已从简单的监控单元进化为具备预测性维护能力的智能中枢。其核心算法架构如同人体的神经系统,通过实时…

2026/7/6 23:16:04
Unicode 与 HTML 实体编码:5个常见乱码场景分析与解决方案

Unicode 与 HTML 实体编码:5个常见乱码场景分析与解决方案

Unicode 与 HTML 实体编码:5个常见乱码场景分析与解决方案在Web开发中,特殊字符的显示问题就像一场无声的战争——当版权符号©变成问号、欧元符号€显示为乱码时,用户体验和品牌专业性都会受到直接影响。这些看似简单的字符背后&#xf…

2026/7/6 23:16:04
如何高效使用QMK Toolbox:免费开源机械键盘固件刷写完全指南

如何高效使用QMK Toolbox:免费开源机械键盘固件刷写完全指南

如何高效使用QMK Toolbox:免费开源机械键盘固件刷写完全指南 【免费下载链接】qmk_toolbox A Toolbox companion for QMK Firmware 项目地址: https://gitcode.com/gh_mirrors/qm/qmk_toolbox QMK Toolbox是一款专为机械键盘爱好者设计的免费开源工具&#x…

2026/7/6 23:16:04
终极VLC美化方案:5分钟快速上手VeLoCity皮肤

终极VLC美化方案:5分钟快速上手VeLoCity皮肤

终极VLC美化方案:5分钟快速上手VeLoCity皮肤 【免费下载链接】VeLoCity-Skin-for-VLC Castom skin for VLC Player 项目地址: https://gitcode.com/gh_mirrors/ve/VeLoCity-Skin-for-VLC 想让VLC播放器焕然一新吗?VeLoCity皮肤为你带来简单快速的…

2026/7/6 23:11:00

月新闻