作者:radexp,原文鏈接,原文日期:2015/12/14
譯者:Prayer;校對:Channe;定稿:numbbbbb
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)
}
}
這樣寫有兩個好處:
首先,在方法開頭進行條件的檢查,而不是將其包裹在整個的 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)
}
可以用 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)
}
進階 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")
然而,如果判斷的條件不僅是簡單的布爾表達式而涉及到可選值的解包,可以使用 guard
:
guard let kittens = internet.kittens else {
fatalError("OMG ran out of kittens!")
}
斷言
有時候,總是期望在某種條件能夠被滿足,然而即使條件不滿足也不是什么大不了的程序錯誤。在這種情況下,可以考慮像下面這樣使用 assertionFailure
:
guard let puppies = internet.puppies else {
assertionFailure("Huh, no dogs")
return nil
}
通過這種 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
}
對這種簡單的情況而言,使用兩個分支的 if..else
語句比起沒有分支的 guard
更加容易理解。雖然可能在其他的情形中使用 guard
也是一個很好的候選項。
// Better!
var projectName: String? {
if let project = task.project where project.isValid {
return project.name
} else {
return nil
}
}
進階 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)
}
不要:在 guard
的 else
語句中放入復雜代碼
這是上面這些原則的推論:
guard
的 else
語句中,除了一個簡單的提前退出語句外,不應該有其他的代碼邏輯。加入一些診斷日志的代碼是可以的,但是其他的代碼邏輯不應該有。當然也可以在 else
中加入一些對未完成工作的清理或者打開資源的釋放,雖然大部分情況下,你應該使用 defer
來完成這些清理工作。
總之,如果你在 else
塊做了任何實際功能,除了那些離開當前方法的必要操作,你就誤用了 guard
。
**經驗之談: **guard
的 else
代碼塊不要多于 2-3 行代碼。
本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 http://swift.gg。