- 原文地址:Using errors as control flow in Swift
- 原文做者:John Sundell
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Swants
- 校對者:Bruce-pac, iWeslie
咱們在 App 和系統中對控制流的管理方式,會對咱們代碼的執行速度、Debug 的難易程度等方方面面產生巨大影響。咱們代碼中的控制流本質上是咱們各類方法函數和語句的執行順序,以及代碼最終將會進入到哪一個流程分支。前端
Swift 爲咱們提供了不少定義控制流的工具 —— 如 if
, else
和 while
語句,還有相似 Optional 這樣的結構。這周讓咱們將目光放在如何使用 Swift 內置的錯誤拋出和處理 Model,以使咱們可以更輕鬆地管理控制流。android
Optional 做爲一種重要的語言特性,也是數據建模時處理字段缺失的一種良好方式。在涉及到控制流的特定函數內卻也成了大量重複樣板代碼的源頭。ios
下面我寫了個函數來加載 App Bundle 內的圖片,而後調整圖片尺寸並渲染出來。因爲上面每一步操做都會返回一張可選值類型的圖片,所以咱們須要使用幾回 guard
語句來指出函數可能會在哪些地方退出:git
func loadImage(named name: String, tintedWith color: UIColor, resizedTo size: CGSize) -> UIImage? {
guard let baseImage = UIImage(named: name) else {
return nil
}
guard let tintedImage = tint(baseImage, with: color) else {
return nil
}
return resize(tintedImage, to: size)
}
複製代碼
上面代碼面對的問題是咱們實際上在兩處地方用了 nil
來處理運行時的錯誤,這兩處地方都須要咱們爲每步操做結果進行解包,而且還使引起 error 的語句變得無從查找。github
讓咱們看看如何經過 error 重構控制流來解決這兩個問題,而不是使用拋出函數。咱們將從定義一個枚舉開始,它包含圖像處理代碼中可能發生的每一個錯誤的狀況——看起來像這樣:swift
enum ImageError: Error {
case missing
case failedToCreateContext
case failedToRenderImage
...
}
複製代碼
例如,下面是咱們如何快速更新 loadImage(named:) 來返回一個非可選的 UIImage 或拋出 ImageError.missing:後端
private func loadImage(named name: String) throws -> UIImage {
guard let image = UIImage(named: name) else {
throw ImageError.missing
}
return image
}
複製代碼
若是咱們用一樣的手法修改其它圖像處理函數,咱們就能在高層次的函數上也作出相同改變 —— 刪除全部可選值並保證它要麼返回一個正確的圖像,要麼拋出咱們一系列的操做中產生的任何 error:api
func loadImage(named name: String, tintedWith color: UIColor, resizedTo size: CGSize) throws -> UIImage {
var image = try loadImage(named: name)
image = try tint(image, with: color)
return try resize(image, to: size)
}
複製代碼
上面代碼的改動不只讓咱們的函數體變得更加簡單,並且 Debug 的時候也變得更加輕鬆。由於當發生問題時將會返回咱們明肯定義的錯誤,而不是去找出究竟是哪一個操做返回了 nil。閉包
然而咱們可能對 一直 處理各類錯誤沒有絲毫興趣,因此咱們就不須要在咱們代碼中處處使用 do, try, catch
語句結構,(諷刺的是,這些語句也一樣會產生大量咱們最初要避免的模板代碼)。函數
開心的是當須要使用 Optional 的時候咱們均可以回過頭來用它 —— 甚至包括在使用拋出函數的時候。咱們惟一須要作的就是在須要調用拋出函數的地方使用 try?
關鍵字,這樣咱們又會獲得一開始那樣可選值類型的結果:
let optionalImage = try? loadImage(
named: "Decoration",
tintedWith: .brandColor,
resizedTo: decorationSize
)
複製代碼
使用 try?
的好處之一就是它把世界上最棒的兩件事融合到了一塊兒。咱們既能夠在調用函數後獲得一個可選值類型結果 —— 與此同時又讓咱們可以使用拋出 error 的優勢來管理咱們的控制流 👍。
接下來,讓咱們看下在驗證輸入時使用 error 能夠多大程度上改善咱們的控制流。即便 Swift 已是一個很是有優點而且強類型的環境,它也不能一直保證咱們的函數收到驗證過的輸入值 —— 有些時候使用運行時檢查是咱們惟一能作的。
讓咱們看下另外一個例子,在這個例子中,咱們須要在註冊新用戶時驗證用戶的選擇,在以前的時候,咱們的代碼經常使用 guard
語句來驗證每條規則,當錯誤發生時輸出一條錯誤信息 —— 就像這樣:
func signUpIfPossible(with credentials: Credentials) {
guard credentials.username.count >= 3 else {
errorLabel.text = "Username must contain min 3 characters"
return
}
guard credentials.password.count >= 7 else {
errorLabel.text = "Password must contain min 7 characters"
return
}
// Additional validation
...
service.signUp(with: credentials) { result in
...
}
}
複製代碼
即便咱們只驗證上面的兩條數據,咱們的驗證邏輯也比咱們咱們預期中的增加快。當這種邏輯和咱們的 UI 代碼混合在一塊兒時(特別是同處在一個 View Controller 中)也讓整個測試變得更加困難 —— 因此讓咱們看看是否能夠把一些代碼解耦以使控制流更加完善。
理想狀況下,咱們但願驗證代碼只被咱們本身持有,這樣就能使開發和測試相互隔離,而且可以使咱們的代碼變得更易於重用。爲了達到這個目的,咱們爲全部的驗證邏輯建立一個公用類型來包含驗證代碼的閉包。咱們能夠稱這個類型爲驗證器,並將它定義爲一個簡單的結構體並讓它持有針對給出 Value
類型進行驗證的閉包:
struct Validator<Value> {
let closure: (Value) throws -> Void
}
複製代碼
使用上面的代碼,咱們就把驗證函數重構爲當一個輸入值沒有經過驗證時拋出一個 error。然而,爲每個驗證過程定義一個新的 Error
類型可能會再次引起產生沒必要要模板代碼的問題(特別是當咱們僅僅只是想爲用戶展現出來一個錯誤而已時)—— 因此讓咱們引入一個寫驗證邏輯時只須要簡單傳遞一個 Bool
條件和一條當發生錯誤時展現給用戶信息的函數:
struct ValidationError: LocalizedError {
let message: String
var errorDescription: String? { return message }
}
func validate( _ condition: @autoclosure () -> Bool,
errorMessage messageExpression: @autoclosure () -> String
) throws {
guard condition() else {
let message = messageExpression()
throw ValidationError(message: message)
}
}
複製代碼
上面咱們又使用了 @autoclosure,它是讓咱們在閉包內自動解包的推斷語句。查看更多信息,點擊 "Using @autoclosure when designing Swift APIs"。
有了上述條件,咱們如今能夠實現共用驗證器的所有驗證邏輯 —— 在 Validator
類型內構造計算靜態屬性。例如,下面是咱們如何實現密碼驗證的:
extension Validator where Value == String {
static var password: Validator {
return Validator { string in
try validate(
string.count >= 7,
errorMessage: "Password must contain min 7 characters"
)
try validate(
string.lowercased() != string,
errorMessage: "Password must contain an uppercased character"
)
try validate(
string.uppercased() != string,
errorMessage: "Password must contain a lowercased character"
)
}
}
}
複製代碼
最後,讓咱們建立另外一個 validate
重載函數,它的做用有點像 語法糖,讓咱們在有須要驗證的值和要使用的驗證器的時候去調用它:
func validate<T>(_ value: T, using validator: Validator<T>) throws {
try validator.closure(value)
}
複製代碼
全部代碼都寫好了,讓咱們修改須要調用的地方以使用新的驗證系統。上述方法的優雅之處在於,雖然須要一些額外的類型和一些基礎準備,但它使咱們的驗證輸入值的代碼變得很是漂亮而且整潔:
func signUpIfPossible(with credentials: Credentials) throws {
try validate(credentials.username, using: .username)
try validate(credentials.password, using: .password)
service.signUp(with: credentials) { result in
...
}
}
複製代碼
也許還能作的更好點,咱們能夠經過使用 do, try, catch
結構調用上面的 signUpIfPossible
函數將全部驗證錯誤的邏輯放在一個單獨的地方 —— 這時咱們就只須要向用戶顯示拋出錯誤的描述信息:
do {
try signUpIfPossible(with: credentials)
} catch {
errorLabel.text = error.localizedDescription
}
複製代碼
值得注意的是,雖然上面的代碼示例沒有使用任何本地化,但咱們老是但願在真實應用程序中向用戶顯示全部錯誤消息時使用本地化字符串。
圍繞可能遇到的錯誤構建代碼的另外一個好處是,它一般使測試更加容易。因爲一個拋出函數本質上有兩個不一樣的可能輸出 —— 一個值和一個錯誤。在許多狀況下,覆蓋這兩個場景去添加測試是很是直接的。
例如,下面是咱們如何可以很是簡單地爲咱們的密碼驗證添加測試 —— 經過簡單地斷言錯誤用例確實拋出了一個錯誤,而成功案例沒有拋出錯誤,這就涵蓋了咱們的兩個需求:
class PasswordValidatorTests: XCTestCase {
func testLengthRequirement() throws {
XCTAssertThrowsError(try validate("aBc", using: .password))
try validate("aBcDeFg", using: .password)
}
func testUppercasedCharacterRequirement() throws {
XCTAssertThrowsError(try validate("abcdefg", using: .password))
try validate("Abcdefg", using: .password)
}
}
複製代碼
如上面代碼所示,因爲 XCTest
支持拋出測試功能 —— 而且每一個未被處理的錯誤都會做爲一個失敗 —— 咱們惟一須要作的就是使用 try
來調用咱們的 validate
函數驗證用例是否成功,若是沒有拋出錯誤咱們就測試成功了 👍。
在 Swift 代碼中其實有不少種方式來管理控制流 —— 不管操做成功仍是失敗,使用 error 結合拋出函數是一個很是好的選擇。雖然這樣作的時候會須要一些額外的操做(如引入 error 類型並使用 try
或 try?
來調用函數)—— 可是讓咱們的代碼簡潔起來真的會帶來極大的提高。
函數將可選類型做爲返回結果固然也是值得提倡的 —— 特別是在沒有任何合理的錯誤能夠拋出的狀況下,可是若是咱們須要在幾處地方同時爲可選值使用 guard
語句進行判斷,那麼使用 error 替代可能給咱們帶來更清晰的控制流。
你是什麼想法呢? 若是你如今正在使用 error 結合拋出函數來管理你代碼中的控制流 —— 或者你正在嘗試其餘方案?請在 Twitter @johnsundell 告訴我,期待你的疑問、評論和反饋。
感謝閱讀!🚀
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。