
一句話預告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 實時驗證編譯器生成結果。