在 Swift 程式碼中,閉包無所不在:排序條件、網路完成處理常式、按鈕動作,甚至 SwiftUI 的 body。傳遞大括號區塊的程式碼每天會出現數十次。但要說明「閉包擷取值究竟是什麼意思」、「[weak self]為什麼要用」以及「@escaping 為什麼需要標註」,很多人就會一時語塞。
這三個問題其實是一體的。理解閉包如何擷取周遭值,就能連帶理解它為何是參考型別、為何會產生循環參考,以及為何需要 escaping 標註。Swift 基礎系列第 2 篇,本文將依序梳理這條關聯。
什麼是閉包——比名稱更重要的是「擷取」這項特性
先用最少的篇幅整理語法。閉包是把可執行的程式碼區塊當成值處理的語法。它可以存入變數、作為引數傳遞,也可以回傳。
let add: (Int, Int) -> Int = { a, b in a + b }
add(2, 3) // 5
其實,以 func 宣告的函式也是具名閉包。在 Swift 中,函式和閉包屬於同一類型;{ } 語法只是無名稱、即時建立的版本。
不過,closure 這個名稱並非來自「程式碼區塊」,而是來自另一項特性:包住並關閉周遭變數,也就是 close over。
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter = makeCounter()
counter() // 1
counter() // 2
counter() // 3
奇妙的事情正在發生。count 是 makeCounter 的區域變數,函式回傳後本應消失,卻在每次呼叫回傳的閉包時持續增加。閉包擷取了自身範圍外的變數 count,因此函式結束後仍讓它存活。這就是閉包的本質,也是本文其餘內容的共同源頭。
精確理解擷取——不是複製,而是參考
Swift 閉包預設以參考方式擷取。它不是帶走一份值的複本,而是維持與該變數本身的連結。因此會得到這樣的結果。
var multiplier = 2
let times = { (n: Int) in n * multiplier }
times(10) // 20
multiplier = 3
times(10) // 30 — 閉包會看到變更後的值
使用的不是建立閉包時的 multiplier(2),而是執行時的 multiplier(3)。閉包攜帶的不是變數快照,而是變數本身。
如果想固定建立當下的值,就使用擷取清單。
let times = { [multiplier] (n: Int) in n * multiplier }
times(10) // 20
multiplier = 3
times(10) // 20 — 固定為建立當下的 2
寫在方括號中的變數會在建立閉包時被複製,並以常數形式封存在閉包內。總結如下:預設是參考擷取(保持活躍的連結),擷取清單是值擷取(建立時的快照)。在說明 [weak self] 之前,必須先釐清這個差異。
也正因為有這個擷取儲存區,閉包才是參考型別。被擷取的變數必須與閉包共享生命週期,因此會存放在堆積上;複製閉包值時,只是多了一個共享該儲存區的參考。這就是在以 struct 為中心的 Swift 中,閉包會像類別一樣運作的原因。如果不熟悉值型別與參考型別,建議先閱讀值型別優先的文章。
循環參考與 [weak self]——擷取造成的典型問題
參考擷取的代價就是循環參考。結構總是相同。
class ProfileViewModel {
var onUpdate: (() -> Void)?
var name = ""
func bind() {
onUpdate = {
print("名稱: \(self.name)")
}
}
}
ViewModel 透過 onUpdate 屬性強烈持有閉包,但該閉包又以參考方式擷取 self,也就是 ViewModel。ViewModel → 閉包 → ViewModel 的持有環就此閉合;在 ARC 世界中,兩者會彼此抓住,永遠無法釋放。即使關閉畫面,ViewModel 仍留在記憶體中,洩漏就完成了。
解法是擷取清單的第二種用途。[weak self] 表示要以弱參考擷取 self。閉包會抓住 self,但不擁有它,因此能切斷環。代價是 self 可能先被釋放,所以在閉包內 self 會變成選用型別,通常會以 guard let self else { return } 開始。選用型別篇介紹的提早返回模式,在這裡再次派上用場。
onUpdate = { [weak self] in
guard let self else { return }
print("名稱: \(self.name)")
}
重點是,[weak self] 不是萬用前綴。循環成立的條件是「self 擁有閉包,而閉包擷取 self」。沒有這個條件就不需要 weak。例如傳給 DispatchQueue.main.asyncAfter 的閉包會在執行後被系統丟棄,因此 self 只是暫時多存活一會兒,並不是洩漏。不要反射式地加上 weak,應養成先問「誰會擁有這個閉包、擁有多久」的習慣。像 NSTimer 這種持有結構特殊而廣為人知的案例,會在另一篇文章介紹。
@escaping——閉包比函式活得更久時
最後一塊拼圖是:作為函式參數接收的閉包,有時會標註 @escaping。
func fetchUser(completion: @escaping (User) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, _ in
let user = parse(data)
completion(user) // 在函式回傳很久後才執行
}.resume()
}
區分標準是執行時機。函式回傳前在函式內執行完就丟棄的閉包,是 non-escaping(預設值);若閉包被存入屬性或傳給非同步工作,可能在函式回傳後執行,就是 escaping,也就是逃出函式範圍的閉包。
為什麼要強制區分?因為是否逃逸會改變編譯器與開發者的判斷方式。non-escaping 保證只在函式執行期間存活,編譯器可以最佳化擷取儲存,也從根本上排除循環參考的疑慮。因此,把閉包傳給 map 或 filter 時,不必特別擔心 self。相反地,escaping 表示閉包會被存放在某處並長期存活,因此需要檢查前述的循環參考。@escaping 就像 API 層級的警告標籤,提醒你「這個閉包會存活很久,請注意擷取」。
補充一點:completion handler 風格的 escaping 閉包,隨著 async/await 引入,在新程式碼中逐漸減少;但若要閱讀並橋接既有 API,仍必須準確理解這個概念。
三個要熟記的判斷基準
把理論濃縮成實務準則,就是三點。
第一,看到閉包先問生命週期。 這個閉包是在函式內使用完就結束(non-escaping),還是會被存放並長期存在(escaping)?這一問就能決定需要多在意擷取。
第二,使用 weak 要靠判斷,不要靠反射。 真正會閉合持有環的地方(存入屬性的處理常式、類似委派的回呼)使用 [weak self];沒有環的一次性執行則不需要。若難以判斷,可用 Instruments 的 Leaks 或 deinit 記錄確認。
第三,用擷取清單記錄意圖。 想固定值就用 [value];不想擁有它就用 [weak self]。擷取清單首先不是效能工具,而是把「這個閉包如何連結外部世界」明確寫進程式碼的文件。
總結
- 閉包的本質不是程式碼區塊,而是擷取:包住並關閉周遭變數,延長其生命週期的特性。
- 預設擷取不是複製而是參考,因此閉包會看到執行時的值;擷取清單 [x] 則會將值固定為建立時的狀態。
- 由於必須共享擷取儲存區,閉包是參考型別。
- self 所擁有的閉包若擷取 self,就會形成循環參考;[weak self] + guard let self 是標準解法。但若不存在持有環,就不需要 weak。
- @escaping 是「比函式活得更久的閉包」這個警告標籤,用來標示需要檢查擷取的地方。
下一篇將整理儲存屬性、計算屬性、lazy,以及屬性觀察器等,隱藏在一行變數宣告中的選項。

![[Swift 基礎 #2] 閉包擷取、weak self 與 escaping 封面圖](/assets/images/posts/acbedb03-d119-4e28-b248-ff183d5af89c/1.jpg)