NSError ** 與 throws 的三個問題

KyXu發表於2019-03-04

問題一:為什麼有錯誤處理還要返回值?

NSFileManager 裡面有這樣一個方法:

- (BOOL)removeItemAtURL:(NSURL *)URL error:(NSError **)error;複製程式碼

使用的時候我們會傳入一個 &error 再獲取這個錯誤值,來看這個過程中有沒有什麼錯誤,那麼通過 error == nil 不就可以知道是否執行成功嗎,為什麼需要 BOOL 返回值,這是一個冗餘的設計嗎?

考慮下面這種情況:

NSData *data = nil;
NSError *error = nil;
BOOL success = [data writeToURL:nil options:NSDataWritingAtomic error:&error];複製程式碼

我們會發現,由於 data 是 nil,這個方法會直接返回 0,但是 error 依然是 nil,所以官方文件也要求我們一定要通過返回值判斷是否執行成功,而不是僅僅去對 error 判空。

另外,基於 Objective-C 的語言特性,這裡我們無法阻止呼叫者對 error 引數傳遞 nil,但是這個方法在這種情況下依然需要告知呼叫者是否執行成功,所以返回值是一個必要的設計。

然而,下面我們會發現,雖然這不是一個冗餘設計,但是這也不是一個好的設計。


問題二:如何做出一個沒有返回值的錯誤處理?

上面那個方法在 Swift 中是這樣的:

func removeItem(atPath path: String) throws複製程式碼

沒有返回值

Objective-C 中為了對外部建立的 NSError 賦值,使用了雙指標設計,即 NSError *__autoreleasing*,這種做法在 Swift 語言中,變成了 inout 關鍵字:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}複製程式碼

這實現了在函式中修改引數值,按照這種寫法,是不是我們可以臆想出一種完全對應於 Objective-C 風格的版本:

func removeItem(atPath path: String) throws // 原版
func removeItem(atPath path: String, error: inout NSError) -> Bool // 臆想版本複製程式碼

理論上或許可行,但是這裡我臆想出的這個版本,和 OC 中這個方法的設計,都是不好的設計:為了方便,很多時候開發者會對 error 傳入
nil,這使得一旦出錯,這裡的 Error Handling 是無效的,而當初這裡
傳入 nil 也正是因為開發者認為這種同步方法不像非同步的網路請求那樣容易出錯,最終就是艱難的 bug 排查。

Swift 2 引入的異常機制強迫我們使用下面的這種做法,

let fileManager = FileManager.default
do {
    try fileManager.removeItem(atPath: filePath)
} catch {
    print(error)
}複製程式碼

這樣使得錯誤更加容易被發現和處理,並且由於 Swift 是強型別語言,在這裡 nil 並不能執行 removeItem 方法,所以在這裡,沒有返回值卻成了合理的設計。

但有一點需要注意,在這裡我們只能獲取到一個 error,我們卻無法知道可以獲取到一個什麼樣的 error,我們無法直接通過 API 知道,假如這裡 removeItem 不成功,到底可能是因為什麼樣的原因而導致不成功。


問題三:throws 是同步的,非同步的時候怎麼辦?

答:向 Error? 低頭。

func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask複製程式碼

error An error object that indicates why the request failed, or nil if the request was successful.

由於 try catch 是一種同步的語法,在非同步的時候,我們還是隻能通過 Error 或者 NSError 來判斷執行是否成功。

一種更好的做法其實是封裝列舉,像這樣:

enum JSONError: Error {
    case noSuchKey(String)
    case typeMismatch
}複製程式碼

對於這種做法可以參考 antitypical/Result,而如果你一定要使用原生 API,記得看一眼文件吧,到底 return value、error、responseData 中哪個值可以保證你的操作是成功的。


參考連結:

相關文章