代錯誤處理范式)
1. 項目概述為什么C11的異常處理值得你重新審視如果你寫過C尤其是經(jīng)歷過C98/03時代那么對try、catch、throw這幾個關鍵字一定不陌生。但很多人對異常的態(tài)度是“知道有這么個東西能不用就不用”或者僅僅停留在“捕獲一下別讓程序崩潰”的層面。到了C11異常機制其實經(jīng)歷了一次重要的“現(xiàn)代化”升級它不僅僅是語法上的小修小補更帶來了一套更安全、更高效、與現(xiàn)代C設計哲學如RAII、移動語義深度集成的錯誤處理范式。理解C11的異常意味著你能寫出更健壯、更清晰、更易于維護的代碼尤其是在資源管理、庫接口設計和大型項目協(xié)作中。這篇文章我們就來徹底拆解C11的異常機制從為什么需要它到怎么用好它再到如何避開那些教科書里不提的“坑”。2. C11異常機制的核心設計哲學2.1 從錯誤碼到異常一次思維模式的轉(zhuǎn)變在傳統(tǒng)的C風格或早期C中錯誤處理主要依賴返回值錯誤碼和全局變量如errno。這種方式有幾個固有的缺陷侵入性每個可能出錯的函數(shù)調(diào)用后都必須立即檢查返回值。這導致業(yè)務邏輯代碼被大量的if (ret ! SUCCESS)語句割裂可讀性差。易被忽略調(diào)用者可以輕易地“忘記”檢查錯誤碼程序會帶著錯誤狀態(tài)繼續(xù)運行導致后續(xù)更難以追蹤的故障。多層傳遞困難在深層嵌套的函數(shù)調(diào)用中錯誤需要一層層手動向上傳遞中間每一層都需要處理錯誤碼代碼冗余。C異常機制的核心思想是將錯誤處理路徑與正常執(zhí)行路徑分離。當函數(shù)遇到無法就地處理的錯誤時它不返回而是“拋出”throw一個異常對象。這個異常會沿著調(diào)用棧向上“冒泡”直到被某個能夠處理它的catch塊“捕獲”。在這個過程中中間的函數(shù)無需關心錯誤細節(jié)只需確保自身資源被正確清理這通常由析構函數(shù)自動完成。這使得正常業(yè)務邏輯的代碼流保持清晰而錯誤處理邏輯被集中到專門的catch塊中。2.2 C11帶來的關鍵增強noexcept與移動語義的協(xié)同C11并沒有改變異常的基本語法try/catch/throw但它引入了兩個至關重要的特性深刻影響了異常的使用方式noexcept說明符這是對已棄用的“動態(tài)異常規(guī)范”throw(type)的現(xiàn)代化替代。noexcept是一個布爾屬性它向編譯器和使用者承諾這個函數(shù)不會拋出任何異常。這帶來了兩大好處優(yōu)化機會編譯器知道noexcept函數(shù)是“異常安全”的可以生成更高效的代碼在某些情況下如標準庫容器操作可以避免生成復雜的棧展開代碼。接口契約它成為了函數(shù)接口的一部分調(diào)用者可以依賴這個承諾。例如移動構造函數(shù)和移動賦值運算符通常被標記為noexcept這允許標準庫容器如std::vector在擴容時安全地使用移動而非拷貝從而提升性能。如果一個被聲明為noexcept的函數(shù)拋出了異常程序會直接調(diào)用std::terminate()終止這是一種強契約。異常與移動語義在C11之前異常安全代碼的編寫嚴重依賴“拷貝交換”copy-and-swap慣用法。有了移動語義后我們可以編寫出更高效的異常安全代碼。例如在實現(xiàn)“強異常安全保證”操作要么完全成功要么完全失敗對象狀態(tài)不變時可以先用移動操作準備新狀態(tài)再用noexcept的交換操作來提交更改這比拷貝整個對象開銷小得多。2.3 異常安全保證的三個級別使用異常時必須明確你的函數(shù)提供哪種級別的異常安全保證這是設計可靠接口的基礎基本保證如果操作因異常而中斷程序仍處于有效狀態(tài)沒有資源泄漏但對象的具體狀態(tài)可能是未知的。強保證事務安全操作要么完全成功要么完全失敗對象狀態(tài)保持不變。這通常通過“要么全做要么不做”的模式實現(xiàn)例如先在一個臨時對象上完成所有可能拋異常的操作再用noexcept的swap來提交。不拋擲保證noexcept保證承諾操作絕不會失敗絕不會拋出異常。析構函數(shù)、移動操作、交換操作等通常應努力達到此級別。在C11中由于noexcept的引入和移動語義的普及實現(xiàn)“強保證”和“不拋擲保證”比以往更容易也更重要。3. 核心語法、標準異常類與資源管理3.1 基礎語法再探與最佳實踐雖然語法基礎但細節(jié)決定成敗。// 拋出異常通過值拋出通常拋出標準異常類或從其派生的自定義異常。 void processFile(const std::string filename) { std::ifstream file(filename); if (!file.is_open()) { // 拋出標準異常包含錯誤信息 throw std::runtime_error(無法打開文件: filename); } // ... 處理文件可能拋出其他異常如bad_alloc } // 捕獲異常通過常量引用捕獲。避免切片也避免不必要的拷貝。 int main() { try { processFile(data.txt); // 可能拋出其他異常的操作 riskyOperation(); } catch (const std::runtime_error e) { // 捕獲特定異常 std::cerr 運行時錯誤: e.what() \n; return 1; } catch (const std::exception e) { // 捕獲所有標準異常 std::cerr 標準異常: e.what() \n; return 2; } catch (...) { // 捕獲所有其他任何類型的異常慎用 std::cerr 發(fā)生了未知異常\n; return -1; } return 0; }關鍵細節(jié)與最佳實踐拋出什么優(yōu)先拋出派生自std::exception的標準庫異常類型如std::runtime_error,std::invalid_argument,std::out_of_range或自定義的、繼承自它們的類型。這保證了所有異常都能通過std::exception的引用被捕獲并且可以使用what()方法獲取信息。避免拋出內(nèi)置類型如int,char*因為它們攜帶的信息太少且不符合標準異常體系。如何捕獲始終通過常量引用const 捕獲異常。這避免了兩個問題一是“對象切片”如果捕獲基類對象派生類的額外信息會丟失二是不必要的拷貝構造開銷因為異常對象可能在棧展開過程中被多次傳遞。catch(...)的使用這個“捕獲所有”的處理器應謹慎使用。它通常只用在程序的最外層用于記錄日志并執(zhí)行有序關閉防止未處理的異常導致程序靜默崩潰。在中間層使用catch(...)會吞噬所有異常使得上層無法獲知具體的錯誤類型不利于調(diào)試和恢復。3.2 標準庫異常體系解析C標準庫定義了一個清晰的異常類層次結構理解它有助于你拋出和捕獲更有意義的異常。std::exception ├── std::logic_error (邏輯錯誤通常在編碼階段可避免) │ ├── std::invalid_argument (參數(shù)無效) │ ├── std::domain_error (參數(shù)值在函數(shù)定義的域外) │ ├── std::length_error (試圖創(chuàng)建超出最大大小的對象) │ └── std::out_of_range (參數(shù)值超出有效范圍如vector::at) ├── std::runtime_error (運行時錯誤通常在運行時環(huán)境導致) │ ├── std::range_error (計算結果超出有意義的范圍) │ ├── std::overflow_error (算術上溢) │ ├── std::underflow_error (算術下溢) │ └── std::system_error (系統(tǒng)相關錯誤C11新增包含錯誤碼) └── std::bad_alloc (內(nèi)存分配失敗通常從operator new拋出)選擇指南參數(shù)檢查失敗如負數(shù)傳給要求正數(shù)的函數(shù) -std::invalid_argument索引越界 -std::out_of_range打開不存在的文件、網(wǎng)絡連接失敗 -std::runtime_error或其派生類如std::system_error內(nèi)存不足 -std::bad_alloc(通常自動拋出)C11新增的std::system_error尤其有用它可以封裝操作系統(tǒng)錯誤碼如errno讓你能拋出和捕獲帶系統(tǒng)錯誤信息的異常。3.3 RAII異常安全的基石異常安全的核心挑戰(zhàn)在于當異常拋出導致棧展開時如何確保所有已分配的資源內(nèi)存、文件句柄、鎖、網(wǎng)絡連接等都被正確釋放答案是RAII。RAII將資源的管理綁定到對象的生命周期上。資源在構造函數(shù)中獲取在析構函數(shù)中釋放。由于C保證棧上對象的析構函數(shù)在棧展開時會被自動調(diào)用無論退出方式是正常返回還是異常因此資源總能被正確清理。class FileHandle { public: explicit FileHandle(const char* filename) : handle(std::fopen(filename, r)) { if (!handle) { throw std::runtime_error(打開文件失敗); } } ~FileHandle() { if (handle) std::fclose(handle); } // 禁用拷貝提供移動移動操作通常應為noexcept FileHandle(const FileHandle) delete; FileHandle operator(const FileHandle) delete; FileHandle(FileHandle other) noexcept : handle(other.handle) { other.handle nullptr; } FileHandle operator(FileHandle other) noexcept { if (this ! other) { if (handle) std::fclose(handle); handle other.handle; other.handle nullptr; } return *this; } // 使用資源的接口 void readData() { /* 使用handle */ } private: std::FILE* handle nullptr; }; void useFile() { FileHandle fh(data.txt); // 資源在構造函數(shù)中獲取 fh.readData(); // 使用資源 // 無論此處是否拋出異常fh的析構函數(shù)都會被調(diào)用文件會被關閉。 // 這就是“基本異常安全保證”。 }實操心得在現(xiàn)代C中你應該幾乎永遠不需要手動new/delete或malloc/free。使用std::unique_ptr,std::shared_ptr,std::vector,std::string等智能指針和容器它們都是RAII的完美體現(xiàn)。對于文件、鎖等使用標準庫提供的RAII包裝器如std::fstream,std::lock_guard或自己編寫小型RAII類。這是寫出異常安全代碼的最重要習慣。4.noexcept的深入理解與實戰(zhàn)應用4.1noexcept的兩種形式與含義noexcept有兩種用法noexcept說明符作為函數(shù)聲明的一部分指明該函數(shù)是否可能拋出異常。void mayThrow(); // 可能拋出 void willNotThrow() noexcept; // 承諾絕不拋出 void conditionalNoexcept(int x) noexcept(x 0); // 條件性noexceptC11起如果noexcept函數(shù)拋出了異常程序會調(diào)用std::terminate()立即終止。這是一種嚴格的契約。noexcept運算符這是一個編譯期運算符用于查詢一個表達式是否聲明為noexcept。static_assert(noexcept(std::swap(a, b)), swap should be noexcept); if constexpr (noexcept(T())) { // C17起編譯期分支 // 使用更高效的路徑 }4.2 為什么移動操作應該盡可能是noexcept這是C11異常機制與性能優(yōu)化結合的關鍵點。標準庫容器如std::vector在需要重新分配內(nèi)存時例如push_back導致容量不足需要將舊元素移動到新內(nèi)存中。為了提供強異常安全保證如果移動中拋出異常容器狀態(tài)不變?nèi)萜餍枰酪苿硬僮魇欠駮伋霎惓?。如果移動構造函?shù)是noexcept的容器會安全地使用移動操作效率高。如果移動構造函數(shù)不是noexcept的容器為了安全起見會回退到使用拷貝操作即使拷貝更慢但拷貝構造函數(shù)通常能提供更強的異常安全保證假設資源類型支持。因此為你自定義的、管理資源的類實現(xiàn)noexcept的移動構造函數(shù)和移動賦值運算符是使其與標準庫高效協(xié)作的關鍵。class MyResource { int* data; public: // 移動構造函數(shù)標記為noexcept MyResource(MyResource other) noexcept : data(std::exchange(other.data, nullptr)) {} // 移動賦值運算符也應為noexcept MyResource operator(MyResource other) noexcept { if (this ! other) { delete[] data; // 假設當前持有資源 data std::exchange(other.data, nullptr); } return *this; } // ... 其他成員 };4.3 如何決定一個函數(shù)是否為noexcept這是一個設計決策。遵循以下原則析構函數(shù)必須總是noexcept。標準庫假設所有析構函數(shù)都是noexcept的如果析構函數(shù)拋出異常程序通常會直接終止且資源清理會出問題。移動操作和swap函數(shù)盡可能使其為noexcept。這是為了與標準庫高效協(xié)作。簡單getter/setter、數(shù)學運算如果只是返回成員變量或進行不涉及資源分配的計算可以標記為noexcept。其他函數(shù)如果函數(shù)內(nèi)部只調(diào)用了其他noexcept函數(shù)并且沒有可能拋出的操作如new可能拋bad_alloc、動態(tài)轉(zhuǎn)換dynamic_cast可能拋bad_cast則可以標記為noexcept。不確定時不要標記noexcept是一個承諾。如果你不能100%確定函數(shù)不會拋出就不要標記它。錯誤的noexcept聲明比沒有聲明更危險。5. 異常處理的高級話題與性能考量5.1 異常規(guī)格Exception Specifications的演進與棄用C98/03引入了動態(tài)異常規(guī)格例如void func() throw(std::exception);意思是func只能拋出std::exception或其派生類型的異常。如果拋出其他類型會調(diào)用std::unexpected()。這套機制在實踐中被證明是笨重且低效的主要問題在于運行時檢查違反規(guī)格是在運行時發(fā)現(xiàn)的而非編譯期。優(yōu)化阻礙編譯器難以優(yōu)化。維護負擔函數(shù)簽名和實現(xiàn)必須嚴格匹配拋出的異常類型。因此在C11中動態(tài)異常規(guī)格除了throw()被標記為棄用deprecated并在C17中移除。throw()被noexcept替代。noexcept是編譯期屬性更簡單也給了編譯器更大的優(yōu)化空間。5.2 異常的性能開銷到底有多大這是一個經(jīng)典問題。異常機制的運行時開銷主要來自兩個方面無異常時的開銷零開銷原則在現(xiàn)代編譯器的實現(xiàn)中如果沒有任何異常被拋出異常處理機制通常幾乎沒有運行時開銷。這主要通過“表格驅(qū)動”的方法實現(xiàn)編譯器會生成額外的靜態(tài)數(shù)據(jù)異常處理表來指導棧展開但正常執(zhí)行路徑的代碼不會被插入額外的檢查指令。這是“零開銷抽象”原則的體現(xiàn)——你不為用不到的功能付費。拋出和捕獲異常時的開銷當異常被拋出時開銷是顯著的。這個過程包括構造異常對象可能在堆上。遍歷調(diào)用棧查找匹配的catch塊。在棧展開過程中調(diào)用所有局部對象的析構函數(shù)。跳轉(zhuǎn)到catch塊的位置。結論與建議不要將異常用于常規(guī)控制流。異常是為異常錯誤情況設計的。如果你預計某個“錯誤”會頻繁發(fā)生例如解析用戶輸入時格式錯誤使用錯誤碼或std::optional/std::expectedC23可能更合適性能更好。對于真正的、罕見的、不可恢復的或需要跨多層調(diào)用處理的錯誤如內(nèi)存耗盡、文件系統(tǒng)錯誤、網(wǎng)絡連接中斷異常是更清晰、更安全的選擇。此時的性能開銷相對于錯誤處理的復雜度和代碼清晰度而言通常是可接受的。在性能極度敏感的熱路徑如高頻交易核心循環(huán)、圖形渲染每幀循環(huán)中需要仔細評估。如果該路徑中任何函數(shù)都可能拋出異常并且異常發(fā)生的頻率不可忽略那么使用異??赡軙蔀槠款i。在這種情況下可以考慮將整個熱路徑封裝起來在最外層統(tǒng)一處理異常或者在該路徑內(nèi)部使用錯誤碼。5.3 自定義異常類的最佳實踐當標準異常類不足以表達你的錯誤時需要自定義異常。// 好的自定義異常示例 class MyNetworkException : public std::runtime_error { public: enum class ErrorCode { Timeout, ConnectionRefused, ProtocolError }; MyNetworkException(ErrorCode code, const std::string message) : std::runtime_error(message), errorCode_(code) {} ErrorCode getErrorCode() const noexcept { return errorCode_; } // 可選重寫what()以提供更豐富的信息 const char* what() const noexcept override { // 注意這里需要小心處理字符串生命周期。簡單做法是返回基類的what()。 // 更復雜的做法可以緩存一個格式化的字符串在成員變量中。 return std::runtime_error::what(); } private: ErrorCode errorCode_; }; // 使用 void connectToServer() { if (timeout) { throw MyNetworkException(MyNetworkException::ErrorCode::Timeout, 連接服務器超時); } }注意事項繼承自標準異常通常從std::runtime_error或std::logic_error派生以便能通過std::exception統(tǒng)一捕獲。提供額外上下文在構造函數(shù)中接受錯誤碼、錯誤信息等存儲為成員變量。小心what()的重寫what()必須返回一個在異常對象生命周期內(nèi)有效的C風格字符串。最簡單的做法是不重寫或者調(diào)用基類的what()。如果需要自定義信息確保返回的指針指向的字符串內(nèi)存是有效的例如指向一個成員std::string的c_str()但要注意異常對象的拷貝/移動問題。遵循“三/五法則”自定義異常類也是類如果需要管理資源要定義好拷貝/移動構造函數(shù)和賦值運算符。通常從標準異常派生而來的類使用編譯器生成的默認版本即可。6. 常見陷阱、調(diào)試技巧與問題排查6.1 典型陷阱與規(guī)避方法在析構函數(shù)中拋出異常這是C中的“未定義行為”觸發(fā)器之一。如果棧展開過程中因另一個異常調(diào)用的析構函數(shù)又拋出了異常程序會直接調(diào)用std::terminate()終止。務必確保析構函數(shù)不會拋出異常。如果析構函數(shù)中的操作可能失敗如關閉文件失敗請吞掉異?;蛴涗浫罩镜灰屗鼈鞑コ鋈ァG衅瑔栴}通過值捕獲異常會導致對象切片。try { throw Derived(); } catch (Base b) { ... } // 錯誤發(fā)生切片Derived部分信息丟失 catch (const Base b) { ... } // 正確通過引用捕獲捕獲所有異常并默默處理catch(...)后如果不做任何有意義處理如記錄日志、重新拋出會隱藏嚴重的程序錯誤使得調(diào)試極其困難。try { /* 復雜操作 */ } catch (...) { // 糟糕什么信息都沒留下 // return false; // 調(diào)用者不知道發(fā)生了什么 } // 改進 catch (const std::exception e) { logError(e.what()); throw; // 或者轉(zhuǎn)換為特定的錯誤碼返回 } catch (...) { logError(Unknown exception); throw; // 重新拋出讓上層處理 }異常安全問題編寫可能拋出異常的函數(shù)時必須考慮所有資源的狀態(tài)。使用RAII是根本解決方案。對于復雜操作考慮使用“copy-and-swap”或“commit-or-rollback”模式來提供強異常安全保證。noexcept誤用給一個實際上可能拋出異常的函數(shù)標記noexcept是埋下了一顆定時炸彈。當它真的拋出異常時程序會立即終止可能連基本的錯誤日志都來不及寫。6.2 調(diào)試與排查技巧利用調(diào)試器大多數(shù)現(xiàn)代調(diào)試器如GDB, LLDB, Visual Studio Debugger都可以設置“在拋出異常時中斷”。這能讓你在異常發(fā)生的第一時間查看調(diào)用棧和變量狀態(tài)是定位問題最直接的方法。獲取調(diào)用棧信息異常拋出點的調(diào)用棧對于定位問題至關重要。雖然C標準沒有提供獲取棧跟蹤的API但可以利用平臺相關功能如Linux的backtrace Windows的CaptureStackBackTrace或第三方庫如Boost.Stacktrace C23可能會正式引入棧跟蹤庫??梢栽谧远x異常類的構造函數(shù)中捕獲并存儲棧信息。使用std::exception_ptr進行異常傳遞有時需要在不同線程間或延遲處理異常。std::exception_ptr可以捕獲任何異常的副本并在之后重新拋出。std::exception_ptr eptr; try { someFunctionThatMayThrow(); } catch (...) { eptr std::current_exception(); // 捕獲當前異常 } // ... 稍后在另一個上下文 if (eptr) { try { std::rethrow_exception(eptr); } catch (const std::exception e) { // 處理異常 } }理解std::terminate和std::set_terminate當異常無法被捕獲如noexcept函數(shù)拋出異?;驐U归_過程中發(fā)生異常沖突時會調(diào)用std::terminate()。你可以通過std::set_terminate設置自己的終止處理器在程序終止前打印一些診斷信息。6.3 異常安全代碼編寫檢查清單在編寫可能拋出異常的代碼時問自己以下幾個問題資源泄漏如果此處拋出異常所有已申請的資源內(nèi)存、句柄、鎖都能正確釋放嗎使用RAII數(shù)據(jù)一致性如果操作中途失敗對象會處于一個有效但錯誤的狀態(tài)嗎提供基本保證還是能完全回滾到操作前的狀態(tài)提供強保證異常傳播這個異常應該由這一層處理還是應該傳遞給更上層的調(diào)用者noexcept正確性這個函數(shù)真的能承諾不拋出任何異常嗎它的所有操作包括它調(diào)用的函數(shù)都是noexcept的嗎析構函數(shù)安全這個類的析構函數(shù)會拋出異常嗎絕對不能7. 現(xiàn)代C中的替代方案與協(xié)同異常并非錯誤處理的唯一方式。C11及后續(xù)標準引入了其他工具它們與異常是互補關系適用于不同場景。7.1std::error_code與system_error對于需要與系統(tǒng)API交互或希望避免異常開銷的底層庫std::error_code是一個輕量級的、不可拋出的錯誤表示方式。它包含一個錯誤碼和一個指向std::error_category的指針后者用于解釋錯誤碼的含義。std::error_code ec; std::filesystem::path p /nonexistent/file; if (!std::filesystem::exists(p, ec)) { if (ec) { std::cout 檢查文件存在時出錯: ec.message() \n; // 不拋出異常繼續(xù)執(zhí)行 } }何時使用在性能關鍵路徑、與C接口交互、或者錯誤是預期內(nèi)且頻繁發(fā)生的情況下使用std::error_code。它可以和異常結合例如庫函數(shù)提供兩個重載一個拋出異常一個接受std::error_code參數(shù)來報告錯誤。7.2std::optional與std::expectedstd::optional(C17)表示一個“可能有值也可能沒有值”的容器。非常適合用于那些可能失敗但不需要額外錯誤信息的操作比如查找一個可能不存在的鍵。std::optionalint parseNumber(const std::string s) { try { return std::stoi(s); } catch (...) { return std::nullopt; // 表示無值 } } auto num parseNumber(abc); if (num) { /* 成功 */ } else { /* 失敗 */ }std::expected(C23)這是std::optional的增強版它不僅可以表示“有值/無值”還可以在“無值”錯誤時攜帶一個錯誤對象。它是錯誤碼和異常之間一個非常優(yōu)雅的折中。std::expectedint, std::string safeDivide(int a, int b) { if (b 0) { return std::unexpected(除數(shù)不能為零); } return a / b; } auto result safeDivide(10, 0); if (result) { /* 使用*result */ } else { std::cout 錯誤: result.error(); }7.3 異常與錯誤處理策略的選擇沒有銀彈。在實際項目中通常采用混合策略應用程序頂層/模塊邊界使用異常。它們能清晰地跨越復雜的調(diào)用鏈將錯誤傳遞到能夠處理的地方如UI層顯示錯誤信息、服務層記錄日志并重試。底層庫、工具函數(shù)根據(jù)情況選擇。如果錯誤是預期內(nèi)的、可恢復的并且調(diào)用者需要立即處理考慮使用std::optional、std::expected或返回錯誤碼。如果錯誤是嚴重的、罕見的或者會破壞不變式使用異常。構造函數(shù)和操作符重載由于它們沒有方便的返回值通常使用異常來報告失敗如內(nèi)存不足、無效參數(shù)。性能極端敏感的核心算法循環(huán)避免在循環(huán)內(nèi)部使用可能拋異常的路徑??梢酝ㄟ^前置檢查、使用noexcept函數(shù)、或?qū)⒄麄€循環(huán)包裹在try-catch塊中來管理。我個人在實際項目中的體會是明確團隊的約定至關重要。是“異常驅(qū)動”還是“錯誤碼驅(qū)動”或是混合模式在模塊接口處如何轉(zhuǎn)換例如底層C風格庫返回錯誤碼中層C包裝器將其轉(zhuǎn)換為異常拋出。統(tǒng)一的錯誤處理策略能極大減少認知負擔和bug。C11提供的這套更完善的異常機制和配套工具讓我們有更多選擇來構建既健壯又高效的軟件系統(tǒng)。最后一個小技巧在編寫庫代碼時考慮同時提供異常和error_code兩種接口給使用者最大的靈活性許多現(xiàn)代C庫如Asio正是這樣做的。