斷言 Assertion

錯誤處理之不要亂用我設計的物件

Liyao Chen
iOS Nerd 🤓
6 min readMay 17, 2018

--

錯誤是很抽象的字,廣義來說所有不在預期內的結果都算是某種”錯誤”。

接下來會分兩三篇文章來分享最近所學到跟錯誤處理相關的議題,話不多說我們開始吧💪

斷言(Assertion) 是一種條件式檢查語法,當現行狀態不合乎條件時會強迫終止程式。

斷言只用在標示一些邏輯上不可能或不應該出現的情形,若上述情形真的出現時,表示有些基本架構已出現問題。 — wiki 斷言

為什麼會需要斷言

當程式碼執行到不該出現的狀態時,且一但進入此狀態就執行不下去的時候。常用在提醒開發者 API 的使用規範、此程式碼缺少必要圖片、初始狀態資料缺少等等。

白話一點就是『這裡有嚴重錯誤,請你現在把程式碼修好』,舉例來說:

  • 汽車應該只能汽油,若是加了沙拉油就會讓車子受損,請不要亂加
  • 初始值應該永遠都要存在,若不存在物件無法建立
  • 必要圖片 (app icon) 一定要包含在資料夾裡
  • function 的輸入值規範在某個範圍 (分數 0–100)
  • 虛擬類別的建立(Swift API 目前無法宣告虛擬類別)

一段設計良好的程式碼應留下足夠的前後文相關線索,提醒使用者(Code user) 物件或方法的使用條件。但我們都知道沒有人喜歡看說明書 (Document),所以更高明的做法就是直接用程式碼規範條件,並且在使用者犯規(不合乎規範)時提醒他,而斷言(Assertion)這個語法就是為此而生。

Assert

預設僅在 Debug 編譯下才會作用,在 Production 沒有效能影響。

Use this function for internal sanity checks that are active during testing but do not impact performance of shipping code. — Apple Document

接著我們來看一段規範輸入範圍的斷言範例

// This method only works when score 0-100 (bounds)
func update(score: Int) {
assert(score >= 0, "score should never be native value")
assert(score <= 100, "score should never greater than 100")

// Prevent crash
let newScore = min(max(0,score), 100)
print(newScore)
}

你可以注意到這裡用 assert 來強制規範 score 必須介於 0 - 100 之間。除此之外,在程式碼後段的 min(max(0,score), 100) 則是用來將範圍外的資料修正,來避免在 production 環境下 Crash 。

AssertionFailure

不該執行到這一行,但是萬一 Production 執行到會忽略

func testAsertionFailure() {
assertionFailure("nope")
println("ever") // never be executed when debug.
}

guard score >= 0, score <= 100 else {
assertionFailure("score out of bounds.")
// If out of bounds in production will be ignored.
return
}

db.write(score: score)

在上面這個範例裡,當數值超過範圍時不會寫入 db ,並且在 Config = DEBUG 時丟出 exception 。

Precondition

跟 Assert 很像但作用在 Debug 及 Release 。

// This method only works in 0-100 (bounds)
func update(score: Int) {
precondition(score >= 0, "score should never be native value")
precondition(score <= 100, "score should never greater than 100")

let newScore = min(max(0, score), 100) // never excute
print(newScore)
}

同樣的例子用 Precondition 的話 let newScore 這行就永遠不會被執行了,因為 Precondition 無論在 DEBUG 或 Release 都會生效。

PreconditionFailure

跟 FatalError() 一樣

guard let config = loadConfig() else {
preconditionFailure("Config couldn't be loaded. Verify that Config.json is Valid")
}

// fatal error: Config couldn’t be loaded. Verify that Config.json is valid.: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17


// [Require] powered by Sundell
// https://github.com/JohnSundell/Require
let config = loadConfig().require(hint: "Verify that Config.json is Valid")

FatalError

當某些區塊不想別人存取時,可以使用 fatalError 來明示。(常用在抽象類別。)

fatalError 還有另一個特性,它可以無視 return 關鍵字,請看下面範例。

// Abstract class
class Animal {
func roar() {
fatalError("Must override this method")
}
}

class Cat: Animal {
override func roar() {
print("Meow!")
}
}

enum MyEnum {
case Value1,Value2,Value3
}

func check(someValue: MyEnum) -> String {
switch someValue {
case .Value1:
return "OK"
case .Value2:
return "Maybe OK"
default:
// 不加 return 也能編譯通過
fatalError("Should not show!")
}
}

它表示不得不實作,但是做一個空實做時,並暗示它永遠不該被使用。

小結

Assertion 是很有用的東西,若是使用得當不僅能夠減少文件跟註解,也能讓後續使用的人能夠有明確指引,並且在超出原始設計的預期範圍能夠及時修正。

有沒有感覺更上一層樓了呢?一起持續學習吧 :D

--

--