Swift Tips - Defer關鍵字
前面有說到,在swift 2.0
引入了guard
關鍵字,可以讓程式碼編寫更流暢。它的優雅簡潔而功能強大確實給了我們極大的方便。具體可以參見Swift-Tips-if-let-var-guard-let-var-%E5%85%A8%E8%A7%A3%E6%9E%90/" target="_blank" rel="nofollow,noindex">這裡
。
而,跟guard
一同引入的還有一個關鍵字 ——defer
。
defer
一句話總結defer
就是:讓執行推遲。
我們知道,在錯誤處理方面,guard
和新的throw
語法之間,Swift 鼓勵用盡早返回錯誤(這也是我最喜歡的方式)來代替巢狀 if 的處理方式。儘早返回讓處理更清晰了,但是已經被初始化(可能也正在被使用)的資源必須在返回前被處理乾淨。
defer
關鍵字為此提供了安全又簡單的處理方式:宣告一個block
,當前程式碼執行的閉包退出時會執行該block
。
具體我們來看下面這個栗子:
舉個栗子
import Darwin func currentHostName() -> String { let capacity = Int(NI_MAXHOST) let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: capacity) //呼叫 gethostname(2) 的函式,用來返回當前系統的主機名稱 guard gethostname(buffer, capacity) == 0 else { buffer.deallocate() return "localhost" } let hostname = String(cString: buffer) buffer.deallocate() return hostname }
這裡有一個在最開始就建立的UnsafeMutablePointer<UInt8>
用於儲存目標資料,但是我既要
在錯誤發生後銷燬它,又要
在正常流程下不再使用它時對其進行銷燬。
這種設計很容易導致錯誤,而且不停地在做重複工作。
通過使用defer
語句,我們可以排除潛在的錯誤並且簡化程式碼:
func currentHostName() -> String { let capacity = Int(NI_MAXHOST) let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: capacity) defer { buffer.deallocate() } guard gethostname(buffer, capacity) == 0 else { return "localhost" } return String(cString: buffer) }
儘管defer
緊接著出現在allocate(capacity:)
呼叫之後,但它要等到當前區域結束時才會被執行。多虧了defer
,buffer
才能無論在哪個點退出函式都可以被釋放。
考慮在任何需要配對呼叫的 API 上都使用defer
,比如allocate(capacity:)
/deallocate()
、wait()
/signal()
和open()
/close()
。這樣的話,你就可以消除一種程式員易犯的錯誤。
多個defer
如果在同一個作用域內使用多個defer
語句,它們會根據出現順序反過來執行——像棧一樣。這個反序是非常重要的細節,保證了被延遲的程式碼塊建立時作用域記憶體在的東西,在程式碼塊執行同樣存在。
舉個例子,執行這段程式碼會得到下面的輸出:
func procrastinate() { defer { print("wash the dishes") } defer { print("take out the recycling") } defer { print("clean the refrigerator") } print("play videogames") } /*輸出結果: play videogames clean the refrigerator take out the recycling wash the dishes */
那麼問題來了,如果你像這樣巢狀defer
語句,會怎麼樣?
defer { defer { print("clean the gutter") } }
哈哈,當然你可以親自試一下。
defer
中的變數
如果在defer
語句中引用了一個變數,執行時會用到變數最終的值。換句話說:defer
程式碼塊不會捕獲變數當前的值。
如果你執行這段程式碼,你會得到下面的輸出:
func flipFlop() { var position = "It's pronounced /ɡɪf/" defer { print(position) } position = "It's pronounced /dʒɪf/" defer { print(position) } } /*輸出結果: It's pronounced /dʒɪf/ It's pronounced /dʒɪf/ */
defer
跳不出作用域
需要注意的是,defer
程式碼塊無法跳出它所在的作用域。因此如你嘗試呼叫一個會 throw 的方法,丟擲的錯誤就無法傳遞到其周圍的上下文。
func burnAfterReading(file url: URL) throws { defer { try FileManager.default.removeItem(at: url) } //Errors not handled let string = try String(contentsOf: url) }
作為替代,你可以使用try?
來無視掉錯誤,或者直接將語句移出defer
程式碼塊,將其放到函式的最後,正常的執行。
defer
並不是總是會執行
看看下面這個栗子:
func foo() { guard false else { return } defer { print("finally") } }
顯然,上面永遠不會執行到defer
這一句,這時候就不會被執行。
這個故事告訴我們,至少要執行到defer
這一行,它才保證後面會觸發。
defer
的壞處
雖然defer
像一個語法糖一樣,但也要小心使用避免形成容易誤解、難以閱讀的程式碼。在某些情況下你可能會嘗試用defer
來對某些值返回之前做最後一步的處理,例如說在後置運算子++
的實現中:
postfix func ++(inout x: Int) -> Int { let current = x x += 1 return current }
在這種情況下,可以用defer
來進行一個很另類的操作。如果能在 defer 中處理的話為什麼要建立臨時變數呢?
postfix func ++(inout x: Int) -> Int { defer { x += 1 } return x }
這種寫法確實聰明,但這樣卻顛倒了函式的邏輯順序,極大降低了程式碼的可讀性。應該嚴格遵循defer
在整個程式最後執行以釋放已申請資源的原則,其他任何使用方法都可能讓程式碼亂成一團。
一臉懵逼是不是?
寫在最後:
「聰明的程式設計師明白自己的侷限性」,我們必須權衡每種語言特性的好處和其成本。
類似於guard
的新特效能讓程式碼流程上更線性,可讀性更高,就應該儘可能使用。
同樣defer
也解決了重要的問題,但是會強迫我們一定要找到它宣告的地方才能追蹤到其銷燬的方法,因為宣告方法很容易被滾動出了視野之外,所以應該儘可能遵循它出現的初衷儘可能少地使用,避免造成混淆和晦澀。