建設網(wǎng)站公開教學視頻下載seo關鍵詞排名優(yōu)化哪家好
? ? ?錯誤處理是編程的一個基本要素。除非你寫的是“hello world”,否則就必須處理代碼中的錯誤。在本文中,我將討論各種編程語言在處理錯誤時使用的最常見的四種方法,并分析它們的優(yōu)缺點。
關注不同設計方案的語法、代碼可讀性、演變過程、運行效率,將有助于我們寫出更為優(yōu)雅和健壯的代碼。
返回錯誤代碼
這是最古老的策略之一——如果一個函數(shù)可能會出錯,它可以簡單地返回一個錯誤代碼——通常是負數(shù)或者null。例如,C 語言中經(jīng)常使用:
FILE*?fp?=?fopen("file.txt"?,?"w");if?(!fp)?{//?發(fā)生了錯誤}
這種方法非常簡單,既易于實現(xiàn),也易于理解。它的執(zhí)行效率也非常高,因為它只需要進行標準的函數(shù)調用,并返回一個值,不需要有運行時支持或分配內存。但是,它也有一些缺點:
用戶很容易忘記處理函數(shù)的錯誤。例如,在 C 中,printf 可能會出錯,但我?guī)缀鯖]有見過程序檢查它的返回值!
如果代碼必須處理多個不同的錯誤(打開文件,寫入文件,從另一個文件讀取等),那么傳遞錯誤到調用堆棧會很麻煩。
除非你的編程語言支持多個返回值,否則如果必須返回一個有效值或一個錯誤,就很麻煩。這導致 C 和 C++ 中的許多函數(shù)必須通過指針來傳遞存儲了“成功”返回值的地址空間,再由函數(shù)填充,類似于:
my_struct?*success_result;int?error_code?=?my_function(&success_result);if?(!error_code)?{//?can?use?success_result}
眾所周知,Go 選擇了這種方法來處理錯誤,而且,由于它允許一個函數(shù)返回多個值,因此這種模式變得更加人性化,并且非常常見:
user,?err?=?FindUser(username)if?err?!=?nil?{return?err}
Go 采用的方式簡單而有效,會將錯誤傳遞到調用方。但是,我覺得它會造成很多重復,而且影響到了實際的業(yè)務邏輯。不過,我寫的 Go 還不夠多,不知道這種印象以后會不會改觀!
異常
異??赡苁亲畛S玫腻e誤處理模式。try/catch/finally 方法相當有效,而且使用簡單。異常在上世紀 90 年代到 2000 年間非常流行,被許多語言所采用(例如 Java、C# 和 Python)。
與錯誤處理相比,異常具有以下優(yōu)點:
它們自然地區(qū)分了“快樂路徑”和錯誤處理路徑
它們會自動從調用堆棧中冒泡出來
你不會忘記處理錯誤!
然而,它們也有一些缺點:需要一些特定的運行時支持,通常會帶來相當大的性能開銷。
此外,更重要的是,它們具有“深遠”的影響——某些代碼可能會拋出異常,但被調用堆棧中非常遠的異常處理程序捕獲,這會影響代碼的可讀性。
此外,僅憑查看函數(shù)的簽名,無法確定它是否會拋出異常。
C++ 試圖通過throws 關鍵字來解決這個問題,但它很少被使用,因此在 C++ 17 中已被棄用 ,并在 C++ 20 中被刪除。此后,它一直試圖引入noexcept 關鍵字,但我較少寫現(xiàn)代 C++,不知道它的流行程度。
(注:throws 關鍵字很少使用,因為使用過于繁瑣,需要在函數(shù)簽名中指定拋出的異常類型,并且這種方法不能處理運行時發(fā)生的異常,有因為“未知異常”而導致程序退出的風險)
Java 曾試圖使用“受檢的異常(checked exceptions)”,即你必須將異常聲明為函數(shù)簽名的一部分——但是這種方法被認為是失敗的,因此像 Spring 這種現(xiàn)代框架只使用“運行時異常”,而有些 JVM 語言(如 Kotlin)則完全拋棄了這個概念。這造成的結果是,你根本無法確定一個函數(shù)是否會拋出什么異常,最終只得到了一片混亂。
(注:Spring 不使用“受檢的異常”,因為這需要在函數(shù)簽名及調用函數(shù)中顯式處理,會使得代碼過于冗長而且造成不必要的耦合。使用“運行時異常”,代碼間的依賴性降低了,也便于重構,但也造成了“異常源頭”的混亂)
回調函數(shù)
另一種方法是在 JavaScript 領域非常常見的方法——使用回調,回調函數(shù)會在一個函數(shù)成功或失敗時調用。這通常會與異步編程結合使用,其中 I/O 操作在后臺進行,不會阻塞執(zhí)行流。
例如,Node.JS 的 I/O 函數(shù)通常加上一個回調函數(shù),后者使用兩個參數(shù)(error,result),例如:
const?fs?=?require('fs');fs.readFile('some_file.txt',?(err,?result)?=>?{if?(err)?{console.error(err);return;}console.log(result);});
但是,這種方法經(jīng)常會導致所謂的“回調地獄”問題,因為一個回調可能需要調用其它的異步 I/O,這可能又需要更多的回調,最終導致混亂且難以跟蹤的代碼。
現(xiàn)代的 JavaScript 版本試圖通過引入promise 來提升代碼的可讀性:
fetch("https://example.com/profile",?{method:?"POST",?//?or?'PUT'}).then(response?=>?response.json()).then(data?=>?data['some_key']).catch(error?=>?console.error("Error:",?error));
promise 模式并不是最終方案,JavaScript 最后采用了由 C#推廣開的 async/await 模式,它使異步 I/O 看起來非常像帶有經(jīng)典異常的同步代碼:
async?function?fetchData()?{try?{const?response?=?await?fetch("my-url");if?(!response.ok)?{throw?new?Error("Network?response?was?not?OK");}return?response.json()['some_property'];}?catch?(error)?{console.error("There?has?been?a?problem?with?your?fetch?operation:",?error);}}
使用回調進行錯誤處理是一種值得了解的重要模式,不僅僅在 JavaScript 中如此,人們在 C 語言中也使用了很多年。但是,它現(xiàn)在已經(jīng)不太常見了,你很可能會用的是某種形式的async/await。
函數(shù)式語言的 Result
我最后想要討論的一種模式起源于函數(shù)式語言,比如 Haskell,但是由于 Rust 的流行,它已經(jīng)變得非常主流了。
它的創(chuàng)意是提供一個Result類型,例如:
enum?Result<S,?E>?{Ok(S),Err(E)}
這是一個具有兩種結果的類型,一種表示成功,另一種表示失敗。返回結果的函數(shù)要么返回一個Ok 對象(可能包含有一些數(shù)據(jù)),要么返回一個Err 對象(包含一些錯誤詳情)。函數(shù)的調用者通常會使用模式匹配來處理這兩種情況。
為了在調用堆棧中拋出錯誤,通常會編寫如下的代碼:
let?result?=?match?my_fallible_function()?{Err(e)?=>?return?Err(e),Ok(some_data)?=>?some_data,};
由于這種模式非常常見,Rust 專門引入了一個操作符(即問號 ?) 來簡化上面的代碼:
let?result?=?my_fallible_function()?;???//?注意有個"?"號
這種方法的優(yōu)點是它使錯誤處理既明顯又類型安全,因為編譯器會確保處理每個可能的結果。
在支持這種模式的編程語言中,Result 通常是一個 monad,它允許將可能失敗的函數(shù)組合起來,而無需使用 try/catch 塊或嵌套的 if 語句。
(注:函數(shù)式編程認為函數(shù)的輸入和輸出應該是純粹的,不應該有任何副作用或狀態(tài)變化。monad 是一個函數(shù)式編程的概念,它通過隔離副作用和狀態(tài)來提高代碼的可讀性和可維護性,并允許組合多個操作來構建更復雜的操作)
根據(jù)你使用的編程語言和項目,你可能主要或僅僅使用其中一種錯誤處理的模式。
不過,我最喜歡的還是 Result 模式。當然,不僅是函數(shù)式語言采用了它,例如,在lastminute.com 中,我們在 Kotlin 中使用了 Arrow 庫,它包含一個受 Haskell 強烈影響的類型Either。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? --END--