使用 guard 的正確姿勢

WilmaBeals 8年前發布 | 15K 次閱讀 技術 Apple Swift開發

來自: 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。

 本文由用戶 WilmaBeals 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!