ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

C# 状态机的三重境界:从手写模式到编译器黑魔法

C# 状态机的三重境界:从手写模式到编译器黑魔法 一句话预告C# 里「会暂停」的代码底层都是状态机在撑腰。本文从「手动存档」到「系统自动存档」把三种状态机形态串成一条完整的认知链。关键词State Pattern · yield return · async/await · 编译器生成代码 · SharpLab 反编译 · 性能陷阱一、引子游戏存档里的状态机玩过 RPG 游戏的都知道「存档点」机制你走到一个存档水晶前按下手柄游戏把当前所有状态血量、位置、道具、剧情进度写进存档文件。关机、吃饭、睡觉游戏世界被「冻结」。下次开机读档游戏从断点精确恢复仿佛时间从未流逝。C# 里的状态机本质上就是编译器替你写的自动存档系统。只不过它存的不是游戏角色而是局部变量的值执行到了哪一行状态号接下来该唤醒哪个等待器awaiter当你写下await或yield return时你其实是在对编译器说「帮我在这儿存个档等条件满足了读档继续。」本文把 C# 状态机拆成三重境界由浅入深帮你建立完整的认知地图。二、第一重境界手写状态机显式存档这是开发者完全掌控的状态机。没有编译器帮忙每一行跳转都要自己写。场景订单生命周期管理一个电商后台的订单会经历以下状态plainCreated已创建 → Paid已支付 → Shipped已发货 → Delivered已签收 ↓ Cancelled已取消代码实现// 状态接口 public interface IOrderState { void Handle(OrderContext context, OrderAction action); string Name { get; } } // 上下文持有当前状态 public class OrderContext { public IOrderState CurrentState { get; set; } public string OrderId { get; set; } public void Transition(OrderAction action) { Console.WriteLine($[{OrderId}] 当前状态: {CurrentState.Name}, 收到动作: {action}); CurrentState.Handle(this, action); Console.WriteLine($[{OrderId}] 切换后状态: {CurrentState.Name}\n); } } // 动作枚举 public enum OrderAction { Pay, Ship, Deliver, Cancel } // 具体状态已创建 public class CreatedState : IOrderState { public string Name Created; public void Handle(OrderContext ctx, OrderAction action) { switch (action) { case OrderAction.Pay: ctx.CurrentState new PaidState(); break; case OrderAction.Cancel: ctx.CurrentState new CancelledState(); break; default: throw new InvalidOperationException($Created 状态不支持 {action}); } } } // 具体状态已支付 public class PaidState : IOrderState { public string Name Paid; public void Handle(OrderContext ctx, OrderAction action) { switch (action) { case OrderAction.Ship: ctx.CurrentState new ShippedState(); break; case OrderAction.Cancel: ctx.CurrentState new CancelledState(); break; default: throw new InvalidOperationException($Paid 状态不支持 {action}); } } } // 具体状态已发货、已签收、已取消省略结构同理 public class ShippedState : IOrderState { public string Name Shipped; public void Handle(OrderContext ctx, OrderAction action) { if (action OrderAction.Deliver) ctx.CurrentState new DeliveredState(); else throw new InvalidOperationException(只能签收); } } public class DeliveredState : IOrderState { public string Name Delivered; public void Handle(OrderContext ctx, OrderAction action) throw new InvalidOperationException(订单已完结); } public class CancelledState : IOrderState { public string Name Cancelled; public void Handle(OrderContext ctx, OrderAction action) throw new InvalidOperationException(订单已取消); }运行演示var order new OrderContext { OrderId ORD-2026-001, CurrentState new CreatedState() }; order.Transition(OrderAction.Pay); // Created → Paid order.Transition(OrderAction.Ship); // Paid → Shipped order.Transition(OrderAction.Deliver); // Shipped → Delivered输出[ORD-2026-001] 当前状态: Created, 收到动作: Pay [ORD-2026-001] 切换后状态: Paid [ORD-2026-001] 当前状态: Paid, 收到动作: Ship [ORD-2026-001] 切换后状态: Shipped [ORD-2026-001] 当前状态: Shipped, 收到动作: Deliver [ORD-2026-001] 切换后状态: Delivered手写状态机的特点表格维度说明控制权完全在开发者手中状态存储显式类实例CreatedState、PaidState跳转逻辑显式switch或字典映射适用场景业务规则复杂、需要人工审核的状态流转缺点样板代码多每新增一个状态要新建一个类三、第二重境界yield return编译器半自动存档手写状态机太啰嗦C# 2.0 引入了yield return编译器开始帮你写状态机——但只帮你一半。场景分页加载大数据流假设你要从数据库读取 100 万条订单但不想一次性全加载进内存public IEnumerableOrder GetOrdersBatch(int batchSize) { int page 0; while (true) { var batch QueryDatabase(page, batchSize); // 每次只查一页 if (batch.Count 0) yield break; // 没数据了终止 foreach (var order in batch) { yield return order; // ← 存档点返回一条暂停下次从这里恢复 } page; Console.WriteLine($[内部] 第 {page} 页加载完成存档等待消费...); } }关键洞察惰性执行// 调用 GetOrdersBatch 时方法体一行都不会执行 // 它只是返回一个「状态机对象」像一张未刮开的彩票。 var stream GetOrdersBatch(batchSize: 100); // 直到 foreach 开始消费状态机才逐步运转 foreach (var order in stream) { Process(order); // 每处理一条状态机向前推进一步 }编译器生成的秘密SharpLab 反编译视角用 SharpLab 反编译上面的代码编译器会生成一个私有类大概长这样csharp[CompilerGenerated] private sealed class GetOrdersBatchd__0 : IEnumerableOrder, IEnumeratorOrder { private int 1__state; // 状态号-2初始, 0第一条, 1第二条... private int page5__1; // 局部变量「提级」为字段 private ListOrder batch5__2; private int i5__3; // foreach 循环变量也被保存 // 核心方法每次 MoveNext() 就是「读档继续」 private bool MoveNext() { switch (1__state) { case 0: page5__1 0; goto case 1; case 1: batch5__2 QueryDatabase(page5__1, 100); if (batch5__2.Count 0) return false; // yield break i5__3 0; goto case 2; case 2: if (i5__3 batch5__2.Count) { page5__1; 1__state 1; // 回到「查下一页」状态 return true; } Current batch5__2[i5__3]; // yield return i5__3; 1__state 2; // 记录下次从 case 2 恢复 return true; } return false; } }图解 yield 状态机调用方 编译器生成的状态机 │ │ │ GetOrdersBatch(100) │ │ ───────────────────────────▶ │ 创建状态机对象state -2 │ ◀─────────────────────────── │ 返回 IEnumerable尚未执行 │ │ │ foreach → MoveNext() │ │ ───────────────────────────▶ │ state 0 → 初始化 page 0 │ ◀─────────────────────────── │ 返回 trueCurrent 第1条订单 │ │ │ 消费订单 1... │ │ │ │ MoveNext() │ │ ───────────────────────────▶ │ state 2 → 返回第2条订单 │ ◀─────────────────────────── │ 返回 trueCurrent 第2条订单 │ │ │ ...直到 batch 耗尽 │ │ │ │ MoveNext() │ │ ───────────────────────────▶ │ page查下一页数据库 │ ◀─────────────────────────── │ 返回 true新 batch或 false结束yield 状态机的特点表格维度说明控制权开发者写「同步风格」代码编译器生成状态机状态存储编译器自动把局部变量「提级」为字段恢复触发外部调用MoveNext()如foreach线程模型同步、单线程、惰性经典用途LINQ、大数据流、无限序列如斐波那契数列四、第三重境界async/await编译器全自动存档yield是单线程内的暂停-恢复。async/await则更进一步跨线程的暂停-恢复。场景并发下载多个文件public async Task DownloadAllAsync(Liststring urls) { var semaphore new SemaphoreSlim(3); // 最多3个并发 var tasks new ListTask(); foreach (var url in urls) { await semaphore.WaitAsync(); // ← 存档点1没许可证就暂停线程去干别的 var task DownloadOneAsync(url, semaphore); tasks.Add(task); } await Task.WhenAll(tasks); // ← 存档点2等全部完成 Console.WriteLine(全部下载完毕); } private async Task DownloadOneAsync(string url, SemaphoreSlim semaphore) { try { Console.WriteLine($[{Thread.CurrentThread.ManagedThreadId}] 开始下载 {url}); var data await _httpClient.GetByteArrayAsync(url); // ← 存档点3网络IO期间线程释放 Console.WriteLine($[{Thread.CurrentThread.ManagedThreadId}] 完成下载 {url}大小 {data.Length}); await File.WriteAllBytesAsync($tmp/{Guid.NewGuid()}.bin, data); // ← 存档点4磁盘IO } finally { semaphore.Release(); // 归还许可证 } }关键洞察线程不是被「阻塞」而是被「释放」没有async/await的时代你要么阻塞线程var data client.GetByteArray(url);—— 线程干等浪费资源回调地狱client.GetAsync(url, callback { ... });—— 代码支离破碎async/await给了第三条路代码像同步一样线性书写执行像异步一样非阻塞。编译器生成的秘密多 await 场景用 SharpLab 反编译一个双 await 方法public async Taskint CalculateAsync() { var a await FetchAAsync(); // 第1个存档点 var b await FetchBAsync(); // 第2个存档点 return a b; }编译器生成一个结构体C# 高版本优化为 struct 减少堆分配[StructLayout(LayoutKind.Auto)] [CompilerGenerated] private struct CalculateAsyncd__0 : IAsyncStateMachine { public int 1__state; // 状态号-2初始, 0await前, 1第1个await后, 2第2个await后 public AsyncTaskMethodBuilderint t__builder; private TaskAwaiterint u__1; // 第1个 awaiter private TaskAwaiterint u__2; // 第2个 awaiter private int a5__1; // 局部变量 a「提级」为字段 void MoveNext() { int result; try { TaskAwaiterint awaiter; switch (1__state) { default: // state -2 或 0从头开始 a5__1 0; awaiter FetchAAsync().GetAwaiter(); if (!awaiter.IsCompleted) { 1__state 0; // 记录下次从 case 0 恢复 u__1 awaiter; // 保存 awaiter t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); return; // ← 线程在此释放 } goto case 0; case 0: // 从第1个 await 恢复 awaiter u__1; u__1 default; 1__state -1; a5__1 awaiter.GetResult(); // 拿到 a 的值 awaiter FetchBAsync().GetAwaiter(); if (!awaiter.IsCompleted) { 1__state 1; u__2 awaiter; t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); return; // ← 再次释放线程 } goto case 1; case 1: // 从第2个 await 恢复 awaiter u__2; u__2 default; 1__state -1; int b awaiter.GetResult(); result a5__1 b; // a b break; } t__builder.SetResult(result); // Task 完成 } catch (Exception ex) { 1__state -2; t__builder.SetException(ex); } } }图解 async 状态机线程池线程 T1 │ │ 调用 CalculateAsync() │ ──────────────────────────────▶ │ 同步执行到第1个 await │ FetchAAsync() 发起网络请求 │ ┌─────────────────────────────┐ │ │ state 0 │ │ │ 保存 awaiter 到 u__1 │ │ │ 注册回调完成后调用 MoveNext│ │ └─────────────────────────────┘ │ ◀────────────────────────────── returnT1 被释放去干别的 │ │ 【网络传输中... 线程 T1 在忙别的】 │ │ 网络数据包到达 │ 回调触发 → MoveNext() │ ┌─────────────────────────────┐ │ │ state 0 → 跳到 case 0 │ │ │ 恢复 u__1拿到 a 的值 │ │ │ 继续执行到第2个 await... │ │ └─────────────────────────────┘ │ 若 FetchBAsync() 也未完成 │ state 1保存 awaiterreturn │ │ 【可能由另一个线程 T2 恢复执行】 │ 这就是 await 的线程无关性async 状态机的特点维度说明控制权开发者写「同步风格」代码编译器处理全部异步细节状态存储局部变量、awaiter、builder 全部「提级」为字段/结构体成员恢复触发被 await 的 Task 完成后通过线程池回调MoveNext()线程模型非阻塞、可跨线程恢复不保证同一个线程经典用途IO 密集型操作网络、磁盘、数据库五、三重境界对比一张图看懂选择┌─────────────────────────────────────────────────────────────────────┐ │ C# 状态机光谱 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 手动控制 ◄────────────────────────────────────────► 全自动 │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ 手写状态机 │ │ yield return │ │ async/await │ │ │ │ State Pattern│ │ 迭代器状态机 │ │ 异步状态机 │ │ │ └──────────────┘ └──────────────┘ └──────────────────┘ │ │ │ │ 显式类switch 编译器生成类 编译器生成结构体/类 │ │ 业务规则复杂时用 大数据流/惰性序列 IO密集型/高并发时用 │ │ │ └─────────────────────────────────────────────────────────────────────┘表格特性手写 State Patternyield returnasync/await谁写状态机开发者编译器编译器状态存储类实例字段编译器生成字段编译器生成字段/结构体暂停触发显式方法调用yield return未完成的await恢复触发显式方法调用MoveNext()Task 完成回调是否释放线程否否是是否跨线程否否是返回值无特定IEnumerableTTask/TaskT堆分配开发者控制类有struct可能无/ 类有六、性能陷阱与最佳实践陷阱 1async 的「无意义税」如果一个方法只是透传 Task不要加async// ❌ 坏编译器生成完整状态机白白分配一次 public async Taskint GetDataAsync() await _repo.QueryAsync(); // ✅ 好直接返回 Task零状态机开销 public Taskint GetDataAsync() _repo.QueryAsync();陷阱 2ConfigureAwait(false) 的上下文陷阱状态机恢复时默认会捕获SynchronizationContext如 WPF 的 UI 线程上下文。这在库代码中会导致性能损失不必要的线程切换死锁风险经典 ASP.NET / WPF 死锁场景// 库代码不操作 UI的正确写法 public async Taskbyte[] DownloadAsync(string url) { var response await _httpClient.GetAsync(url) .ConfigureAwait(false); // ← 不捕获上下文 return await response.Content.ReadAsByteArrayAsync() .ConfigureAwait(false); }但注意顶层 UI 事件处理器不要用ConfigureAwait(false)否则后续代码可能不在 UI 线程// ❌ 错误await 之后 Label.Text 会抛跨线程异常 private async void Button_Click(object sender, EventArgs e) { var data await DownloadAsync().ConfigureAwait(false); label.Text data; // 崩溃不在 UI 线程 } // ✅ 正确UI 层保持默认上下文捕获 private async void Button_Click(object sender, EventArgs e) { var data await DownloadAsync(); // 恢复后仍在 UI 线程 label.Text data; }陷阱 3ValueTask 的滥用与正确使用ValueTaskT是 C# 7.0 引入的优化如果异步操作经常同步完成如缓存命中用ValueTask避免 Task 的堆分配。private readonly Dictionaryint, User _cache new(); // ❌ 坏即使缓存命中也分配 Task 对象 public async TaskUser GetUserAsync(int id) { if (_cache.TryGetValue(id, out var user)) return user; // 同步路径但调用方仍 await 一个 Task return await FetchFromDbAsync(id); } // ✅ 好缓存命中时零分配 public ValueTaskUser GetUserAsync(int id) { if (_cache.TryGetValue(id, out var user)) return new ValueTaskUser(user); // 零堆分配 return new ValueTaskUser(FetchFromDbAsync(id)); }但注意ValueTask只能被消费一次不能多次await也不能.Result。七、SharpLab 实操亲眼看看编译器的手笔推荐一个在线工具SharpLab.io把下面的代码贴进去右侧选择「Run」或「Decompile」你就能亲眼看到编译器生成的状态机using System; using System.Collections.Generic; using System.Threading.Tasks; public class C { // 实验 1yield 状态机 public IEnumerableint GetNumbers() { yield return 1; yield return 2; yield return 3; } // 实验 2async 状态机 public async Taskint ComputeAsync() { var a await Task.FromResult(10); var b await Task.FromResult(20); return a b; } }观察要点搜索GetNumbersd__—— 这就是编译器生成的迭代器状态机类搜索ComputeAsyncd__—— 这就是异步状态机结构体观察1__state字段 —— 状态号是如何递增的观察MoveNext()方法 —— switch/case 就是状态跳转表八、总结状态机的认知升级路线Level 1 ──▶ 手写 State Pattern 理解「状态-事件-转换」三要素 掌握业务状态建模能力 │ ▼ Level 2 ──▶ yield return 理解「惰性执行」和「暂停-恢复」 掌握大数据流、LINQ 原理 │ ▼ Level 3 ──▶ async/await 理解「非阻塞 IO」和「线程释放」 掌握高并发、响应式编程 │ ▼ Level 4 ──▶ 反编译 性能调优 理解编译器生成的真实代码 掌握 ConfigureAwait、ValueTask、状态机分配优化最后送一句话在 C# 里状态机不是「要不要用」的问题而是「你在哪一层用它」的问题。手写时你是架构师yield 时你是指挥官async 时你是受益者——而编译器永远是那个默默替你写存档代码的「幕后英雄」。附录速查表场景推荐方案理由订单/审批流程手写 State Pattern业务规则复杂需要人工干预读取 10 万条数据yield returnIEnumerable惰性加载内存友好并发下载文件async/awaitTask.WhenAll非阻塞 IO线程高效复用高频缓存查询ValueTaskT同步命中时零堆分配库代码无 UIConfigureAwait(false)避免上下文捕获开销UI 事件处理器纯await不用 ConfigureAwait恢复后仍需操作 UI 控件本文代码基于 .NET 8 编写可在 SharpLab.io 实时验证编译器生成结果。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表