異常 (exception) 和錯誤 (error)。javascript
在 Objective-C 開發中,異常每每是由程序員的錯誤致使的 app 沒法繼續運行,好比咱們向一個沒法響應某個消息的NSObject
對象發送了這個消息,會獲得 NSInvalidArgumentException
的異常,並告訴咱們 "unrecognized selector sent to instance";好比咱們使用一個超過數組元素數量的下標來試圖訪問 NSArray
的元素時,會獲得NSRangeException
。相似因爲這樣所致使的程序沒法運行的問題應該在開發階段就被所有解決,而不該當出如今實際的產品中。相對來講,由 NSError
表明的錯誤更多地是指那些「合理的」,在用戶使用 app 中可能遇到的狀況:好比登錄時用戶名密碼驗證不匹配,或者試圖從某個文件中讀取數據生成 NSData
對象時發生了問題 (好比文件被意外修改了) 等等。html
可是 NSError
的使用方式其實變相在鼓勵開發者忽略錯誤。想想在使用一個帶有錯誤指針的 API 時咱們作的事情吧。咱們會在 API 調用中產生和傳遞 NSError
,並藉此判斷調用是否失敗。做爲某個可能產生錯誤的方法的使用者,咱們用傳入 NSErrorPointer
指針的方式來存儲錯誤信息,而後在調用完畢後去讀取內容,並確認是否發生了錯誤。好比在 Objective-C 中,咱們會寫相似這樣的代碼:java
NSError *error; BOOL success = [data writeToFile: path options: options error: &error]; if(error) { // 發生了錯誤 }
這很是棒,可是有一個問題:在絕大多數狀況下,這個方法並不會發生什麼錯誤,而不少工程師也爲了省事和簡單,會將輸入的 error 設爲 nil
,也就是不關心錯誤 (由於可能他們從沒見過這個 API 返回錯誤,也不知要如何處理)。因而調用就變成了這樣:程序員
[data writeToFile: path options: options error: nil];
可是事實上這個 API 調用是會出錯的,好比設備的磁盤空間滿了的時候,寫入將會失敗。可是當這個錯誤出現並讓你的 app 陷入難堪境地的時候,你幾乎無從下手進行調試 -- 由於系統曾經嘗試過通知你出現了錯誤,可是你卻選擇視而不見。編程
在 Swift 2.0 中,Apple 爲這麼語言引入了異常機制。如今,這類帶有 NSError
指針做爲參數的 API 都被改成了能夠拋出異常的形式。好比上面的 writeToFile:options:error:
,在 Swift 中變成了:swift
public func writeToFile(path: String, options writeOptionsMask: NSDataWritingOptions) throws
咱們在使用這個 API 的時候,再也不像以前那樣傳入一個 error 指針去等待方法填充,而是變爲使用 try catch
語句:數組
do { try d.writeToFile("Hello", options: []) } catch let error as NSError { print ("Error: \(error.domain)") }
若是你不使用 try
的話,是沒法調用 writeToFile:
方法的,它會產生一個編譯錯誤,這讓咱們沒法有意無心地忽視掉這些錯誤。在上面的示例中 catch
將拋出的異常 (這裏就是個 NSError
) 用 let 進行了類型轉換,這其實主要是針對 Cocoa 現有的 API 的,是對歷史的一種妥協。對於咱們新寫的可拋出異常的 API,咱們應當拋出一個實現了ErrorType
的類型, enum
就很是合適,舉個例子:xcode
enum LoginError: ErrorType { case UserNotFound, UserPasswordNotMatch } func login(user: String, password: String) throws { //users 是 [String: String],存儲[用戶名:密碼] if !users.keys.contains(user) { throw LoginError.UserNotFound } if users[user] != password { throw LoginError.UserPasswordNotMatch } print("Login successfully.") }
這樣的 ErrorType
能夠很是明確地指出問題所在。在調用時, catch
語句實質上是在進行模式匹配:安全
do { try login("onevcat", password: "123") } catch LoginError.UserNotFound { print("UserNotFound") } catch LoginError.UserPasswordNotMatch { print("UserPasswordNotMatch") } // Do something with login user
若是你以前寫過 Java 或者 C# 的話,會發現 Swift 中的 try catch
塊和它們中的有些不一樣。在那些語言裏,咱們會把可能拋出異常的代碼都放在一個 try 裏,而 Swift 中則是將它們放在 do 中,並只在可能發生異常的語句前添加 try。相比於 Java 或者 C# 的方式,Swift 裏咱們能夠更清楚地知道是哪個調用可能拋出異常,而沒必要逐句查閱文檔。網絡
固然,Swift 如今的異常機制也並非十全十美的。最大的問題是類型安全,不借助於文檔的話,咱們如今是沒法從代碼中直接得知所拋出的異常的類型的。好比上面的 login
方法,光看方法定義咱們並不知道 LoginError
會被拋出。一個理想中的異常 API 可能應該是這樣的:
func login(user: String, password: String) throws LoginError
很大程度上,這是因爲要與之前的 NSError
兼容所致使的妥協,對於以前的使用 NSError
來表達錯誤的 API,咱們所獲得的錯誤對象自己就是用像 domain 或者 error number 這樣的屬性來進行區分和定義的,這與 Swift 2.0 中的異常機制所拋出的直接使用類型來描述錯誤的思想暫時是沒法兼容的。不過有理由相信隨着 Swift 的迭代更新,這個問題會在不久的未來獲得解決。
另外一個限制是對於非同步的 API 來講,拋出異常是不可用的 -- 異常只是一個同步方法專用的處理機制。Cocoa 框架裏對於異步 API 出錯時,保留了原來的 NSError
機制,好比很經常使用的 NSURLSession
中的 dataTask
API:
func dataTaskWithURL(_ url: NSURL, completionHandler completionHandler: ((NSData!, NSURLResponse!, NSError!) -> Void)?) -> NSURLSessionDataTask
對於異步 API,雖然不能使用異常機制,可是由於這類 API 通常涉及到網絡或者耗時操做,它所產生錯誤的可能性要高得多,因此開發者們其實沒法忽視這樣的錯誤。可是像上面這樣的 API 其實咱們在平常開發中每每並不會去直接使用,而會選擇進行一些封裝,以求更方便地調用和維護。一種如今比較經常使用的方式就是藉助於 enum
。做爲 Swift 的一個重要特性,枚舉 (enum) 類型如今是能夠與其餘的實例進行綁定的,咱們還可讓方法返回枚舉類型,而後在枚舉中定義成功和錯誤的狀態,並分別將合適的對象與枚舉值進行關聯:
enum Result { case Success(String) case Error(NSError) } func doSomethingParam(param:AnyObject) -> Result { //...作某些操做,成功結果放在 success 中 if success { return Result.Success("成功完成") } else { let error = NSError(domain: "errorDomain", code: 1, userInfo: nil) return Result.Error(error) } }
在使用時,利用 switch 中的 let 來從枚舉值中將結果取出便可:
let result = doSomethingParam(path) switch result { case let .Success(ok): let serverResponse = ok case let .Error(error): let serverResponse = error.description }
在 Swift 2.0 中,咱們甚至能夠在 enum 中指定泛型,這樣就使結果統一化了。
enum Result<T> { case Success(T) case Failure(NSError) }
咱們只須要在返回結果時指明 T
的類型,就可使用一樣的 Result
枚舉來表明不一樣的返回結果了。這麼作能夠減小代碼複雜度和可能的狀態,同時不是優雅地解決了類型安全的問題,可謂一箭雙鵰。
所以,在 Swift 2 時代中的錯誤處理,如今通常的最佳實踐是對於同步 API 使用異常機制,對於異步 API 使用泛型枚舉。