Friday Q&A 2016-03-04:Swift 斷言

作者:Mike Ash,原文鏈接,原文日期:2016-03-04
譯者:zltunes;校對:Cee;定稿:shanks

斷言是一種非常有用的機制,它可以檢查代碼中的假設(shè)部分,確保錯誤能夠被及時發(fā)現(xiàn)。今天我將探討 Swift 中提供的斷言調(diào)用以及它們的實現(xiàn),這個話題是由讀者 Matthew Young 提出的。

我不會花太多時間討論一般意義上的斷言是什么或者在哪里使用它們。本文將著眼于 Swift 中提供的斷言機制以及一些實現(xiàn)的細節(jié)。如果你想要了解如何在代碼中充分利用斷言,可以閱讀我以前的文章 Proper Use of Asserts(斷言的正確使用)

API

在 Swift 標準庫中有兩個主要的斷言函數(shù)。

第一個函數(shù)被創(chuàng)造性地命名為 assert。調(diào)用時需要一個真命題:

assert(x >= 0) // x 不能為負

該函數(shù)提供一個可選參數(shù),用于命題為假時打印錯誤信息:

assert(x >= 0, "x can't be negative here")

assert 只有在非優(yōu)化構(gòu)建時有效。在開啟優(yōu)化的情況下這行代碼不會被編譯。當存在某些條件計算耗性能,從而拖慢構(gòu)建速度,但這些條件又是有用的,調(diào)試時必須進行檢查,那么斷言的這一特性就顯得很有用了。

有些人傾向僅在調(diào)試版本中使用斷言,理論上調(diào)試的時候去做一些檢查是個好習慣,但最好保證 app 不會在實際使用時崩潰。不管在斷言檢查中有沒有出現(xiàn)過,一旦(在實際使用中)出現(xiàn)錯誤,都會導致非常嚴重的后果。更好的做法是,如果在實際使用時出現(xiàn)錯誤,應用能迅速退出。我們來看一下如何實現(xiàn)。

函數(shù) preconditionassert 非常像,調(diào)用時二者看起來一樣:

precondition(x >= 0) // x 不能為負
precondition(x >= 0, "x can't be negative here")

不同之處在于該函數(shù)在優(yōu)化構(gòu)建條件下也會執(zhí)行檢查。這使得它成為斷言檢查的一個更好的選擇,并且檢查速度足夠快。

盡管 precondition 在優(yōu)化構(gòu)建中有效,在「非檢查(unchecked)」的優(yōu)化構(gòu)建中仍是無效的。「非檢查」的構(gòu)建是通過在命令行指定 -Ounchecked 來實現(xiàn)的。該指令的執(zhí)行不僅會移除 precondition 調(diào)用,還會進行數(shù)組邊界檢查。這是很危險的,除非你別無選擇,不得不執(zhí)行該命令外盡量不要用。

關(guān)于非檢查構(gòu)建有趣的一點是,盡管 precondition 檢查被移除了,優(yōu)化器仍會假設(shè)命題為真,并在此基礎(chǔ)上優(yōu)化下面的代碼。在上述例子中,生成代碼不會再檢查 x 是否為負,但在接下來的編譯中會默認 x >= 0。這一點對于 assert 也是成立的。

這些函數(shù)各自有一個不帶條件的變體,用來標志失敗的情況。上述兩個函數(shù)的變體分別是 assertionFailurepreconditionFailure。當你要進行斷言檢查的條件與該函數(shù)的調(diào)用不太相符時,變體就顯得很有用了。例如:

guard case .Thingy(let value) = someEnum else {
  preconditionFailure("This code should only be called with a Thingy.")
}

優(yōu)化下的行為和帶條件時類似,開啟優(yōu)化時 assertionFailure 不會被編譯,preconditionFailure 則保留,但在「非檢查」優(yōu)化構(gòu)建時仍會被移除。「非檢查」構(gòu)建時,優(yōu)化器假設(shè)這些函數(shù)永遠不會執(zhí)行,并基于該假設(shè)生成代碼。

最后還有個函數(shù) fatalError。該函數(shù)表示出現(xiàn)異常并終止程序,而不管構(gòu)建是否開啟優(yōu)化或檢查。

記錄調(diào)用者信息

當斷言檢查未通過,會得到這樣一條信息:

precondition failed: x must be greater than zero: file test.swift, line 6

程序是如何獲知文件和代碼行的信息的呢?

在 C 語言中,我們將 assert 當做宏指令來用,同時使用 __FILE____LINE__ 這兩個神奇的標識符來獲取信息:

c
#define assert(condition) do { \
  if(!(condition)) { \
      fprintf(stderr, "Assertion failed %s in file %s line %d\n", #condition, __FILE__, __LINE__); \
      abort(); \
  } \
}

這些函數(shù)最終以調(diào)用者的文件和代碼行信息結(jié)尾,就是因為此處的宏定義。Swift 中沒有宏的概念,那該怎么辦?

Swift 中可以使用默認參數(shù)值達到同樣效果。上述神奇的標識符可被當做參數(shù)的默認值使用。如果調(diào)用者沒有提供一個確切的值,便可將調(diào)用者所處的文件及代碼行作為默認值。目前,這兩個神奇的標識符分別是 __FILE____LINE__,但在 Swift 下一版本中會變成 #file#line,更加符合 Swift 風格。

探討實際中的使用前,我們先看看 assert 的定義:

public func assert(
  @autoclosure condition: () -> Bool,
  @autoclosure _ message: () -> String = String(),
  file: StaticString = #file, line: UInt = #line
)

通常情況下,調(diào)用 assert 僅傳遞一個或兩個參數(shù)。fileline 參數(shù)則作為默認值,用來傳遞調(diào)用者的相關(guān)信息。

沒有強制要求必須使用默認值,如果需要的話你可以傳入其他的值。比如:

assert(false, "Guess where!", file: "not here", line: 42)

最終輸出:

assertion failed: Guess where!: file not here, line 42

有種更加實用的用法,你可以寫一個包裝器來保留原始調(diào)用者的信息,例如:

func assertWrapper(
  @autoclosure condition: () -> Bool,
  @autoclosure _ message: () -> String = String(),
  file: StaticString = #file, line: UInt = #line
) {
  if !condition() {
      print("Oh no!")
  }
  assert(condition, message, file: file, line: line)
}

Swift 版的 assert 有個缺陷。上文提到的 C 版本的 assert 提供 #condition關(guān)鍵字,斷言檢查未通過時可以輸出表達式。而在 Swift 中不可以。因此,盡管 Swift 可以打印斷言失敗時的文件和代碼行信息,但用來檢查的表達式是無從獲知的。

自動閉包

上述函數(shù)都使用 @autoclosure 來修飾 conditionmessage 參數(shù),為什么?

先快速回顧一下 @autoclosure。@autoclosure 修飾的無參閉包可作為某個函數(shù)的形參,調(diào)用該函數(shù)時,調(diào)用者提供一個表達式作為實參。這個表達式會被包裝成閉包并傳遞給函數(shù),例如:

func f(@autoclosure value: () -> Int) {
  print(value())
}

f(42)

等價于:

func f(value: () -> Int) {
  print(value())
}

f({ 42 })

為什么要把表達式包裝成閉包傳遞?因為這樣可以讓調(diào)用的函數(shù)來決定表達式具體執(zhí)行的時間。例如,對于實現(xiàn)兩個布爾類型的 && 運算符時,我們可以通過傳入兩個 Bool 參數(shù)實現(xiàn):

func &&(a: Bool, b: Bool) -> Bool {
        if a {
            if b {
                return true
            }
        }
        return false
    }

有些情況下我們直接調(diào)用就可以:

x > 3 && x < 10

但如果右操作數(shù)計算復雜的話是很耗時的:

x > 3 && expensiveFunction(x) < 10

假定左操作數(shù)為 false 時,右操作數(shù)不會被執(zhí)行的話,還有可能直接崩潰掉:

optional != nil && optional!.value > 3

跟 C 語言一樣,Swift 中的 && 也是短路操作符。左操作數(shù)為 false 時就不再計算右操作數(shù)了。因此該表達式在 Swift 中是安全的,但對我們的函數(shù)則不行。@autoclosure 使得函數(shù)可以控制表達式執(zhí)行的時間,保證只有左操作數(shù)為 true的前提下才去執(zhí)行該表達式:

func &&(a: Bool, @autoclosure b: () -> Bool) -> Bool {
  if a {
    if b() {
      return true
    }
  }
  return false
}

現(xiàn)在就符合 Swift 的語義了,當 a 為 false 時 b 永遠不會執(zhí)行。

對斷言而言,則完全是考慮性能問題。因為斷言消息有可能是很耗時的操作。例如:

assert(widget.valid, "Widget wasn't valid: \(widget.dump())")

你肯定不想每次都去計算一長串字符串,即便 widget 是合法、什么都不必輸出的時候。對消息參數(shù)使用 @autoclosure 修飾,assert 便可避免計算 message 表達式,除非當斷言檢查不通過的時候。

條件本身也是 @autoclosure,因為優(yōu)化構(gòu)建下 assert 不會去檢查條件。既然不去檢查,也就不涉及計算了。使用 @autoclosure 意味著不會拖慢優(yōu)化構(gòu)建的速度:

assert(superExpensiveFunction())

本文提到的 API 中的函數(shù)都使用了 @autoclosure 來保證除非不得已情況下,盡量避免參數(shù)的計算。出于某種原因,連 fatalError 都使用了 @autoclosure 修飾,盡管它是無條件執(zhí)行的。

代碼移除

基于代碼的編譯情況,這些函數(shù)會在代碼生成時被移除。它們位于 Swift 標準庫,而不是你自己寫的代碼中,而 Swift 標準庫的編譯遠早于你自己的代碼。這一切是怎么協(xié)調(diào)的?

在 C 語言中,這一切都跟宏相關(guān)。宏僅存在于頭部,因此會在執(zhí)行代碼行的時候編譯,盡管原則上這些代碼隸屬于庫,實際上它們直接被當做你自己的代碼。這意味著它們可以檢查是否設(shè)置了 DEBUG 宏(或者類似標識),如果未設(shè)置就不會生成代碼。例如:

c
#if DEBUG
#define assert(condition) do { \
        if(!(condition)) { \
            fprintf(stderr, "Assertion failed %s in file %s line %d\n", #condition, __FILE__, __LINE__); \
            abort(); \
        } \
    }
#else
#define assert(condition) (void)0
#endif

又一次,在 Swift 中沒有宏的概念,那是怎么做的呢?

如果你看過這些函數(shù)在標準庫中的定義,會發(fā)現(xiàn)它們都用 @_transparent 進行了注釋。該特性使得函數(shù)有點類似于宏。這些函數(shù)的調(diào)用都是內(nèi)聯(lián)的,而不是當做獨立函數(shù)來調(diào)用。當你在 Swift 代碼中寫入 precondition(...) 語句的時候,標準庫中 precondition 的函數(shù)體會被直接插入你的代碼中,就好像你自己復制粘貼過去一樣。這意味著這部分代碼的編譯情況跟其余代碼一樣,優(yōu)化器完全可以看到函數(shù)體內(nèi)的代碼??梢钥吹剑攦?yōu)化開啟的時候 assert 編譯器沒有做任何事,而是被移除掉了。

標準庫是一個獨立的庫,獨立庫中的函數(shù)是怎么內(nèi)聯(lián)進你自己的代碼中的呢?對 C 語言來講,庫中包括編譯對象的代碼,這個問題顯得沒有意義。

Swift 標準庫是一個 .swiftmodule 文件,完全不同于 .dylib 或者 .a 文件。一個 .swiftmodule 文件包含模塊中的所有對象的聲明,也可以包括完整的實現(xiàn)。引用 The module format documentation 中的一句話:

The SIL block contains SIL-level implementations that can be imported into a client's SILModule context.(一個 SIL 塊包括可以被導入到用戶定義的 SILModule 上下文中的 SIL 層實現(xiàn)。)

這意味著這些斷言函數(shù)的函數(shù)體被以一種中間形式保存到標準庫模塊中。之后調(diào)用函數(shù)的時候函數(shù)體內(nèi)的代碼便可被內(nèi)聯(lián)。既然可以被內(nèi)聯(lián),這些代碼也就處于同一編譯環(huán)境下,必要時優(yōu)化器也可以將它們?nèi)恳瞥?/p>

總結(jié)

Swift 提供了一系列好用的斷言函數(shù)。assertassertionFailure 函數(shù)僅在優(yōu)化未開啟時有效。這對于檢查那些耗性能的條件是很有用的,但通常情況下應盡量避免使用。preconditionpreconditionFailure 函數(shù)在優(yōu)化開啟時也有效。

這些函數(shù)對 conditionmessage 的參數(shù)使用了 @autoclosure 修飾,使得函數(shù)可以控制參數(shù)計算的時機。從而避免了每次斷言檢查都去計算自定義的 message,同時也避免了在優(yōu)化開啟,斷言函數(shù)無效時去檢查 condition。

斷言函數(shù)是標準庫的一部分,但它們使用了 @_transparent 修飾,使得生成的中間代碼可以導入到模塊中。當函數(shù)被調(diào)用時,整個函數(shù)體會被內(nèi)聯(lián)至調(diào)用處,因此優(yōu)化器可以在需要的時候移除它們。

今天就講到這里!希望這篇文章可以幫助你在自己的代碼中更大膽地使用斷言。斷言是很有用的機制,它可以讓問題一旦發(fā)生就及時明顯地顯現(xiàn)出來,而不是發(fā)生很久后才顯示出一些“癥狀”。下次會帶來一些更棒的想法。每周周五問答都是基于讀者的一些想法建立的,如果你也有想在這里討論的話題,就快發(fā)過來吧!

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請訪問 http://swift.gg

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內(nèi)容

  • 關(guān)于 Swift 重要這個文檔所包含的準備信息, 是關(guān)于開發(fā)的 API 和技術(shù)的。這個信息可能會改變, 根據(jù)這個文...
    無灃閱讀 4,350評論 1 27
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,807評論 18 139
  • 前言 Swift是一門新的適用于iOS,macOS,watchOS,tvOS開發(fā)的編程語言。盡管如此,Swift的...
    BoomLee閱讀 1,767評論 0 4
  • 目錄Swift學習資料@完整App@App框架@ 響應式框架@ UI@ 日歷三方庫@下拉刷新@模糊效果@富文本@圖...
    IOS開發(fā)攻城獅_Fyc閱讀 6,313評論 1 90
  • Hey~ 這個暑假,我在孩子王南京總部人力資源部門招聘組實習。時間不長,六月份斷斷續(xù)續(xù)的幾天,加上七月、八月兩個月...
    慢慢單讀閱讀 522評論 3 5