Swift中defer的正確使用方法
defer 是干什么用的
很簡單,用一句話概括,就是 defer block 里的代碼會在函數(shù) return 之前執(zhí)行,無論函數(shù)是從哪個分支 return 的,還是有 throw,還是自然而然走到最后一行。
這個關鍵字就跟 Java 里的 try-catch-finally 的finally一樣,不管 try catch 走哪個分支,它都會在函數(shù) return 之前執(zhí)行。而且它比 Java 的finally還更強大的一點是,它可以獨立于 try catch 存在,所以它也可以成為整理函數(shù)流程的一個小幫手。在函數(shù) return 之前無論如何都要做的處理,可以放進這個 block 里,讓代碼看起來更干凈一些~
其實這篇文章的緣起是由于在對 Kingfisher 做重構的時候,因為自己對 defer 的理解不夠準確,導致了一個 bug。所以想藉由這篇文章探索一下 defer 這個關鍵字的一些 edge case。
典型用法
Swift 里的 defer 大家應該都很熟悉了,defer 所聲明的 block 會在當前代碼執(zhí)行退出后被調用。正因為它提供了一種延時調用的方式,所以一般會被用來做資源釋放或者銷毀,這在某個函數(shù)有多個返回出口的時候特別有用。比如下面的通過 FileHandle 打開文件進行操作的方法:
func operateOnFile(descriptor: Int32) { let fileHandle = FileHandle(fileDescriptor: descriptor) let data = fileHandle.readDataToEndOfFile() if /* onlyRead */ { fileHandle.closeFile() return } let shouldWrite = /* 是否需要寫文件 */ guard shouldWrite else { fileHandle.closeFile() return } fileHandle.seekToEndOfFile() fileHandle.write(someData) fileHandle.closeFile() }
我們在不同的地方都需要調用 fileHandle.closeFile() 來關閉文件,這里更好的做法是用 defer 來統(tǒng)一處理。這不僅可以讓我們就近在資源申請的地方就聲明釋放,也減少了未來添加代碼時忘記釋放資源的可能性:
func operateOnFile(descriptor: Int32) { let fileHandle = FileHandle(fileDescriptor: descriptor) defer { fileHandle.closeFile() } let data = fileHandle.readDataToEndOfFile() if /* onlyRead */ { return } let shouldWrite = /* 是否需要寫文件 */ guard shouldWrite else { return } fileHandle.seekToEndOfFile() fileHandle.write(someData) }
defer 的作用域
在做 Kingfisher 重構時,對線程安全的保證我選擇使用了 NSLock 來完成。簡單說,會有一些類似這樣的方法:
let lock = NSLock() let tasks: [ID: Task] = [:] func remove(_ id: ID) { lock.lock() defer { lock.unlock() } tasks[id] = nil }
對于 tasks 的操作可能發(fā)生在不同線程中,用 lock() 來獲取鎖,并保證當前線程獨占,然后在操作完成后使用 unlock() 釋放資源。這是很典型的 defer 的使用方式。
但是后來出現(xiàn)了一種情況,即調用 remove 方法之前,我們在同一線程的 caller 中獲取過這個鎖了,比如:
func doSomethingThenRemove() { lock.lock() defer { lock.unlock() } // 操作 `tasks` // ... // 最后,移除 `task` remove(123) }
這樣做顯然在 remove 中造成了死鎖 (deadlock):remove 里的 lock() 在等待 doSomethingThenRemove 中做 unlock() 操作,而這個 unlock 被 remove 阻塞了,永遠不可能達到。
解決的方法大概有三種:
- 換用 NSRecursiveLock:NSRecursiveLock 可以在同一個線程獲取多次,而不造成死鎖的問題。
- 在調用 remove 之前先 unlock。
- 為 remove 傳入按照條件,避免在其中加鎖。
1 和 2 都會造成額外的性能損失,雖然在一般情況下這樣的加鎖性能微乎其微,但是使用方案 3 似乎也并不很麻煩。于是我很開心地把 remove 改成了這樣:
func remove(_ id: ID, acquireLock: Bool) { if acquireLock { lock.lock() defer { lock.unlock() } } tasks[id] = nil }
很好,現(xiàn)在調用 remove(123, acquireLock: false) 不再會死鎖了。但是很快我發(fā)現(xiàn),在 acquireLock 為 true 的時候鎖也失效了。再仔細閱讀 Swift Programming Language 關于 defer 的描述:
A defer statement is used for executing code just before transferring program control outside of the scope that the defer statement appears in.
所以,上面的代碼其實相當于:
func remove(_ id: ID, acquireLock: Bool) { if acquireLock { lock.lock() lock.unlock() } tasks[id] = nil }
GG 斯密達…
以前很單純地認為 defer 是在函數(shù)退出的時候調用,并沒有注意其實是當前 scope 退出的時候調用這個事實,造成了這個錯誤。在 if,guard,for,try 這些語句中使用 defer 時,應該要特別注意這一點。
defer 和閉包
另一個比較有意思的事實是,雖然 defer 后面跟了一個閉包,但是它更多地像是一個語法糖,和我們所熟知的閉包特性不一樣,并不會持有里面的值。比如:
func foo() { var number = 1 defer { print("Statement 2: \(number)") } number = 100 print("Statement 1: \(number)") }
將會輸出:
Statement 1: 100
Statement 2: 100
在 defer 中如果要依賴某個變量值時,需要自行進行復制:
func foo() { var number = 1 var closureNumber = number defer { print("Statement 2: \(closureNumber)") } number = 100 print("Statement 1: \(number)") } // Statement 1: 100 // Statement 2: 1
defer 的執(zhí)行時機
defer 的執(zhí)行時機緊接在離開作用域之后,但是是在其他語句之前。這個特性為 defer 帶來了一些很“微妙”的使用方式。比如從 0 開始的自增:
class Foo { var num = 0 func foo() -> Int { defer { num += 1 } return num } // 沒有 `defer` 的話我們可能要這么寫 // func foo() -> Int { // num += 1 // return num - 1 // } } let f = Foo() f.foo() // 0 f.foo() // 1 f.num // 2
輸出結果 foo() 返回了 +1 之前的 num,而 f.num 則是 defer 中經過 +1 之后的結果。不使用 defer 的話,我們其實很難達到這種“在返回后進行操作”的效果。
雖然很特殊,但是強烈不建議在 defer 中執(zhí)行這類 side effect。
This means that a defer statement can be used, for example, to perform manual resource management such as closing file descriptors, and to perform actions that need to happen even if an error is thrown.
從語言設計上來說,defer 的目的就是進行資源清理和避免重復的返回前需要執(zhí)行的代碼,而不是用來以取巧地實現(xiàn)某些功能。這樣做只會讓代碼可讀性降低。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關文章
Swift中的可選項Optional解包方式實現(xiàn)原理
這篇文章主要為大家介紹了Swift中的可選項Optional解包方式實現(xiàn)原理示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03Swift利用Decodable解析JSON的一個小問題詳解
這篇文章主要給大家介紹了關于Swift利用Decodable解析JSON的一個小問題的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2018-04-04swift中AnyObject和Any的介紹與區(qū)別詳解
雖然使用swift開發(fā)了一段時間,但是感覺對一些基礎的東西了解不是比較透徹,在查詢了許多資料以后還是打算自己動手記錄一下,下面這篇文章主要給大家介紹了關于swift中AnyObject和Any的介紹與區(qū)別的相關資料,需要的朋友可以參考下。2017-12-12深入理解Swift中單例模式的替換及Swift 3.0單例模式的實現(xiàn)
這篇文章主要給大家介紹了關于Swift中單例模式替換的相關資料,然后又跟大家分享了關于Swift3.0 單例模式實現(xiàn)的幾種方法-Dispatch_Once的內容,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考借鑒,下面來一起看看吧。2017-11-11簡單了解Swift語言中的break和continue語句的用法
這篇文章主要簡單介紹了Swift語言中的break和continue語句的用法,與其他語言的一樣用于循環(huán)語句流程控制,需要的朋友可以參考下2015-11-11