作者:Andrea Bergia
譯者:豌豆花下貓@Python貓
英文:Error handling patterns
轉載請保留作者及譯者資訊!
錯誤處理是編程的一個基本要素,除非你寫的是“hello world”,否則就必須處理代碼中的錯誤,在本文中,我將討論各種編程語言在處理錯誤時使用的最常見的四種方法,并分析它們的優缺點,
關注不同設計方案的語法、代碼可讀性、演變程序、運行效率,將有助于我們寫出更為優雅和健壯的代碼,
回傳錯誤代碼
這是最古老的策略之一——如果一個函式可能會出錯,它可以簡單地回傳一個錯誤代碼——通常是負數或者null,例如,C 語言中經常使用:
FILE* fp = fopen("file.txt" , "w");
if (!fp) {
// 發生了錯誤
}
這種方法非常簡單,既易于實作,也易于理解,它的執行效率也非常高,因為它只需要進行標準的函式呼叫,并回傳一個值,不需要有運行時支持或分配記憶體,但是,它也有一些缺點:
- 用戶很容易忘記處理函式的錯誤,例如,在 C 中,printf 可能會出錯,但我幾乎沒有見程序式檢查它的回傳值!
- 如果代碼必須處理多個不同的錯誤(打開檔案,寫入檔案,從另一個檔案讀取等),那么傳遞錯誤到呼叫堆疊會很麻煩,
- 除非你的編程語言支持多個回傳值,否則如果必須回傳一個有效值或一個錯誤,就很麻煩,這導致 C 和 C++ 中的許多函式必須通過指標來傳遞存盤了“成功”回傳值的地址空間,再由函式填充,類似于:
my_struct *success_result;
int error_code = my_function(&success_result);
if (!error_code) {
// can use success_result
}
眾所周知,Go 選擇了這種方法來處理錯誤,而且,由于它允許一個函式回傳多個值,因此這種模式變得更加人性化,并且非常常見:
user, err = FindUser(username)
if err != nil {
return err
}
Go 采用的方式簡單而有效,會將錯誤傳遞到呼叫方,但是,我覺得它會造成很多重復,而且影響到了實際的業務邏輯,不過,我寫的 Go 還不夠多,不知道這種印象以后會不會改觀!??
例外
例外可能是最常用的錯誤處理模式,try/catch/finally 方法相當有效,而且使用簡單,例外在上世紀 90 年代到 2000 年間非常流行,被許多語言所采用(例如 Java、C# 和 Python),
與錯誤處理相比,例外具有以下優點:
- 它們自然地區分了“快樂路徑”和錯誤處理路徑
- 它們會自動從呼叫堆疊中冒泡出來
- 你不會忘記處理錯誤!
然而,它們也有一些缺點:需要一些特定的運行時支持,通常會帶來相當大的性能開銷,
此外,更重要的是,它們具有“深遠”的影響——某些代碼可能會拋出例外,但被呼叫堆疊中非常遠的例外處理程式捕獲,這會影響代碼的可讀性,
此外,僅憑查看函式的簽名,無法確定它是否會拋出例外,
C++ 試圖通過throws 關鍵字來解決這個問題,但它很少被使用,因此在 C++ 17 中已被棄用 ,并在 C++ 20 中被洗掉,此后,它一直試圖引入noexcept 關鍵字,但我較少寫現代 C++,不知道它的流行程度,
(譯者注:throws 關鍵字很少使用,因為使用過于繁瑣,需要在函式簽名中指定拋出的例外型別,并且這種方法不能處理運行時發生的例外,有因為“未知例外”而導致程式退出的風險)
Java 曾試圖使用“受檢的例外(checked exceptions)”,即你必須將例外宣告為函式簽名的一部分——但是這種方法被認為是失敗的,因此像 Spring 這種現代框架只使用“運行時例外”,而有些 JVM 語言(如 Kotlin)則完全拋棄了這個概念,這造成的結果是,你根本無法確定一個函式是否會拋出什么例外,最終只得到了一片混亂,
(譯者注:Spring 不使用“受檢的例外”,因為這需要在函式簽名及呼叫函式中顯式處理,會使得代碼過于冗長而且造成不必要的耦合,使用“運行時例外”,代碼間的依賴性降低了,也便于重構,但也造成了“例外源頭”的混亂)
回呼函式
另一種方法是在 JavaScript 領域非常常見的方法——使用回呼,回呼函式會在一個函式成功或失敗時呼叫,這通常會與異步編程結合使用,其中 I/O 操作在后臺進行,不會阻塞執行流,
例如,Node.JS 的 I/O 函式通常加上一個回呼函式,后者使用兩個引數(error,result),例如:
const fs = require('fs');
fs.readFile('some_file.txt', (err, result) => {
if (err) {
console.error(err);
return;
}
console.log(result);
});
但是,這種方法經常會導致所謂的“回呼地獄”問題,因為一個回呼可能需要呼叫其它的異步 I/O,這可能又需要更多的回呼,最終導致混亂且難以跟蹤的代碼,
現代的 JavaScript 版本試圖通過引入promise 來提升代碼的可讀性:
fetch("https://example.com/profile", {
method: "POST", // or 'PUT'
})
.then(response => response.json())
.then(data =https://www.cnblogs.com/pythonista/archive/2023/05/08/> data['some_key'])
.catch(error => console.error("Error:", error));
promise 模式并不是最終方案,JavaScript 最后采用了由 C#推廣開的 async/await 模式,它使異步 I/O 看起來非常像帶有經典例外的同步代碼:
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 語言中也使用了很多年,但是,它現在已經不太常見了,你很可能會用的是某種形式的async/await,
函式式語言的 Result
我最后想要討論的一種模式起源于函式式語言,比如 Haskell,但是由于 Rust 的流行,它已經變得非常主流了,
它的創意是提供一個Result型別,例如:
enum Result<S, E> {
Ok(S),
Err(E)
}
這是一個具有兩種結果的型別,一種表示成功,另一種表示失敗,回傳結果的函式要么回傳一個Ok 物件(可能包含有一些資料),要么回傳一個Err 物件(包含一些錯誤詳情),函式的呼叫者通常會使用模式匹配來處理這兩種情況,
為了在呼叫堆疊中拋出錯誤,通常會撰寫如下的代碼:
let result = match my_fallible_function() {
Err(e) => return Err(e),
Ok(some_data) => some_data,
};
由于這種模式非常常見,Rust 專門引入了一個運算子(即問號 ?) 來簡化上面的代碼:
let result = my_fallible_function()?; // 注意有個"?"號
這種方法的優點是它使錯誤處理既明顯又型別安全,因為編譯器會確保處理每個可能的結果,
在支持這種模式的編程語言中,Result 通常是一個 monad,它允許將可能失敗的函陣列合起來,而無需使用 try/catch 塊或嵌套的 if 陳述句,
(譯者注:函式式編程認為函式的輸入和輸出應該是純粹的,不應該有任何副作用或狀態變化,monad 是一個函式式編程的概念,它通過隔離副作用和狀態來提高代碼的可讀性和可維護性,并允許組合多個操作來構建更復雜的操作)
根據你使用的編程語言和專案,你可能主要或僅僅使用其中一種錯誤處理的模式,
不過,我最喜歡的還是 Result 模式,當然,不僅是函式式語言采用了它,例如,在我的雇主 lastminute.com 中,我們在 Kotlin 中使用了 Arrow 庫,它包含一個受 Haskell 強烈影響的型別Either,我有計劃寫一篇關于它的文章,最后感謝你閱讀這篇文章,敬請保持關注??,
譯注:還有一篇《Musings about error handling mechanisms in programming languages》文章,同樣分析了不同編程語言在錯誤處理時的方案,它還介紹了 Zig 編程語言的做法、Go 語言的 defer 關鍵字等內容,可以豐富大家對這個話題的理解,推薦一讀,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/551949.html
標籤:其他
上一篇:Python + Selenium,分分鐘搭建 Web 自動化測驗框架!
下一篇:返回列表
