使用 guard 的正確姿勢
來自: http://swift.gg/2016/02/14/swift-guard-radix/
guard 是 Swift 2 中我最喜愛的特性之一。雖然完全不使用 guard 也沒有什么影響,它只是給我們提供了更微妙的句法表達,但是如果能夠正確使用 guard 語句,無疑是一件令人愉快的事。它可以讓我們的方法表意更加明確,更易于閱讀,它能夠表達『提前退出』的意圖,同時提高了程序的健壯性。
因此,學習和理解如何正確使用 guard 表達式非常重要。 guard 有它適用的場景,但是這并不意味著要將所有的 if..else 和 if let 語句都替換成 guard 語句。雖然 guard 語句很棒,但是很容易被濫用,并不是所有的代碼結構中都適合使用 guard 語句。
下面是 guard 語句的使用原則。
可以用 guard :在驗證入口條件時
這可能是最簡單和最常用的情況。你寫了一個方法來完成某個工作,但是只有在滿足某些先決條件的情況下,方法才能夠被繼續執行。
例如:
func updateWatchApplicationContext() { let session = WCSession.defaultSession() guard session.watchAppInstalled else { return } do { let context = ["token": api.token] try session.updateApplicationContext(context) } catch { print(error) } }
</div>
這樣寫有兩個好處:
首先,在方法開頭進行條件的檢查,而不是將其包裹在整個的 if 語句之中。這樣一眼就能看出,這個條件檢查并不是函數本身的功用,而是函數執行的先決條件。
其次,使用 guard 語句時,讀者和編譯器就會知道如果條件為 false ,方法將會直接 return 。雖然這只是對編譯器檢查的一個細微的說明,但是從長遠來看,代碼的可維護性得到了加強——如果有人不小心將提前退出的語句從 else 表達式中移除了,編譯器會及時告訴你這個錯誤。
可以用 guard :在成功路徑上提前退出
使用場景:方法中存在非常長的執行路徑,在最終結果得到之前,中間會有多個需要被滿足的條件,這些條件都應該為真,否則應該直接 return 或者拋出異常。
func vendAllNamed(itemName: String) throws { guard isEnabled else { throw VendingMachineError.Disabled } let items = getItemsNamed(itemName) guard items.count > 0 else { throw VendingMachineError.OutOfStock } let totalPrice = items.reduce(0, combine: +) guard coinsDeposited >= totalPrice else { throw VendingMachineError.InsufficientFunds } coinsDeposited -= totalPrice removeFromInventory(itemName) dispenseSnacks(items) }
</div>
可以用 guard :在可選值解包時(拍扁 if let..else 金字塔)
可能的場景:需要確保執行的先決條件,或者需要在很長的執行路徑中,確保某些檢查點的條件能夠滿足。但是和一些返回 boolean 類型的普通檢查不同,你想要確保某些可選值非空且需要將它解包。
func taskFromJSONResponse(jsonData: NSData) throws -> Task { guard let json = decodeJSON(jsonData) as? [String: AnyObject] else { throw ParsingError.InvalidJSON } guard let id = json["id"] as? Int, let name = json["name"] as? String, let userId = json["user_id"] as? Int, let position = json["pos"] as? Double else { throw ParsingError.MissingData } return Task(id: id, name: name, userId: userId, position: position) }
</div>
進階 Tip:在 Swift 中更好的處理 JSON 方式可以 參考這里
使用 guard 的方式來解包可選值是非常推薦的。 if let 的方式需要在大括號內使用解包之后的值,而 guard 語句將解包之后的值添加到了之后的作用域之中——所以你可以在使用 guard 解包之后直接使用它,不用包裹在大括號內。
我們更推薦使用 guard 的方式,因為如果你有多個需要解包的可選值,使用 guard 的方式可以避免金字塔災難(多個層級的 if let 嵌套)
對我們的大腦來說,在簡單的情況下,理解一個扁平的代碼路徑相比于理解分析嵌套的分支結構更為容易。
可以用 guard : return 和 throw 中
提前退出,作為一種通用的適用規則,表示是以下三種情形之一:
執行被終止
當方法沒有返回值,方法僅執行一個命令,但是該命令無法被完成時。
例子:一個用來更新 WatchKit 應用程序上下文的方法,但是這個應用沒有被部署到 Apple Watch 上去。
推薦做法:直接返回
計算的結果為空值
方法會返回某些值,例如將輸入的參數做某些轉化,而轉化沒有被正確的執行。
例子:方法將反序列化緩存,返回一個對象數組,但是磁盤中的相應緩存不存在。
推薦的做法:
- return nil
- return [] , return "" — 返回標準庫容器的空值
- return Account.guestAccount() — 返回相應對象中,表示為默認或者為空的狀態的值
執行出現錯誤
方法有可能因為多種原因執行失敗,而同時想告知方法的調用者,這些失敗的原因。
例子:方法從磁盤上讀取文件內容,或者進行網絡請求并解析獲得的數據
推薦的做法:
- throw FileError.NotFound
- return Result.Failure(.NotFound) — 如果你要使用指定類型的返回值
- onFailure(.NotFound); return — 適用于異步調用
- return Promise(error: FileError.NotFound) — 在異步調用中使用 Promises 的情況
可以用 guard :日志、崩潰和斷言中
日志
有時候,在方法返回之前有必要將日志信息輸出到控制臺,至少在開發階段這種方式非常有用。即使在我們的代碼能夠很好地處理錯誤情況下,也能夠幫助我們跟蹤錯誤信息。然而,在 guard 的 else 語句中包含太多的處理代碼是不太合適的。
致命狀態(Fatal conditions)
程序的執行的條件不能夠被滿足,如果這是個非常嚴重的程序錯誤,那么故意讓這種狀況 crash 掉,這種處理方式將非常有意義。如果你的應用無論哪種方式都會 crash 掉,又或者程序最終會處于一種非法的狀態的話,這種情形最好自己去處理。通過 guard 的方式,你可以確保程序在可知的情況下退出,在 crash 的時候能夠顯示相應的原因。
這種的使用場景通常是 precondition :
precondition(internet.kittenCount == Int.max, "Not enough kittens in the internet")
</div>
然而,如果判斷的條件不僅是簡單的布爾表達式而涉及到可選值的解包,可以使用 guard :
guard let kittens = internet.kittens else { fatalError("OMG ran out of kittens!") }
</div>
斷言
有時候,總是期望在某種條件能夠被滿足,然而即使條件不滿足也不是什么大不了的程序錯誤。在這種情況下,可以考慮像下面這樣使用 assertionFailure :
guard let puppies = internet.puppies else { assertionFailure("Huh, no dogs") return nil }
</div>
通過這種 crash 的方式,可以在開發和內測期間很容易的找到 bug 位置,但是在正式發布的時候,應用不會 crash 掉(雖然可能 bug 滿天飛)。
在提醒一次,如果判斷的條件僅僅是個布爾類型,使用 assert(condition) 就可以勝任。
不要用 guard :替代瑣碎的 if..else 語句
如果有一個簡單的方法,只包含一個簡單的 if..else 語句,不要使用 guard:
// Don't: var projectName: String? { guard let project = task.project where project.isValid else { return nil } return project.name }
</div>
對這種簡單的情況而言,使用兩個分支的 if..else 語句比起沒有分支的 guard 更加容易理解。雖然可能在其他的情形中使用 guard 也是一個很好的候選項。
// Better! var projectName: String? { if let project = task.project where project.isValid { return project.name } else { return nil } }
</div>
進階 Tip:請確保自己理解了 可選鏈 : Optional.map 和 Optional.flatMap ;通過使用這些工具,通常可以避免使用顯式的 if let 來解包。
不要用 guard :作為 if 的相反情況
在一些語言中,例如 Ruby,有 unless 語句,本質上是 if 的相反情況(reverse if)——作用域內的代碼只有在傳遞進來的條件被判斷為 false 的時候執行。
Swift 中的 guard ,雖然有一些類似,但是它們是不同的東西。 guard 不是通常意義上的分支語義。它特別強調,在某些期望的條件不滿足時,提前退出。
雖然在一些情況下,你可以將 guard 強行掰彎,當做 reverse if 來使用,但是,親不要啊!使用 if..else 語句或者考慮將代碼分割成多個函數。
// Don't: guard let s = sequence as? Set<Element> else { for item in sequence { insert(item) } return } switch (s._variantStorage) { case .Native(let owner): _variantStorage = .Native(owner) case .Cocoa(let owner): _variantStorage = .Cocoa(owner) }
</div>
不要:在 guard 的 else 語句中放入復雜代碼
這是上面這些原則的推論:
guard 的 else 語句中,除了一個簡單的提前退出語句外,不應該有其他的代碼邏輯。加入一些診斷日志的代碼是可以的,但是其他的代碼邏輯不應該有。當然也可以在 else 中加入一些對未完成工作的清理或者打開資源的釋放,雖然大部分情況下,你應該使用 defer 來完成這些清理工作。
總之,如果你在 else 塊做了任何實際功能,除了那些離開當前方法的必要操作,你就誤用了 guard 。
經驗之談: guard 的 else 代碼塊不要多于 2-3 行代碼。
本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問http://swift.gg。