延遲執行的 Swift 計算若其工作強烈捕獲了擁有者,就會讓擁有者持續存活。使用弱捕獲,讓擁有者能在等待期間消失;若計算應在不依賴擁有者的情況下完成,則捕獲獨立的值快照。
對於正在開發小型桌面計算預覽的 Mac 開發者,實際要做的是刻意選擇這個生命週期。此範例會短暫等待、將合成值 [4, 8, 12] 相加,並格式化總和。所有操作都在本機記憶體中執行。
計算應採用哪種生命週期?
Swift 捕獲清單可讓你指定閉包要保留哪些內容。強捕獲會保留被參照的實例;弱參照允許該實例被釋放,之後會變成 nil。這些都是文件所述的語言行為,與計算等待多久無關。Automatic Reference Counting
選擇捕獲語法之前,先決定所需的契約。下表將這些語言規則套用到延遲執行的桌面預覽:
| 預期行為 | 捕獲策略 | 呼叫端釋放擁有者後會發生什麼事? |
|---|---|---|
| 使用同一個擁有者完成工作 | [self] |
工作會保留擁有者,讓它可供計算使用。 |
| 僅在擁有者仍可用時繼續 | [weak self] |
等待後,工作會檢查擁有者是否仍存在。 |
| 使用排程時選定的值完成工作 | [samples = self.samples] |
工作會保留捕獲的值,但不會保留此擁有者。 |
快照列有一個重要前提:此範例捕獲的是整數陣列。建立閉包時,就會計算捕獲清單中的運算式。若改為捕獲類別實例,保留的是該實例的參照,而不是其可變內容的獨立副本。Expressions
此外還有一個不同的值問題。以下強捕獲與弱捕獲版本會在延遲後讀取擁有者的值。快照版本則使用排程當下的值。如果預覽輸入可能在等待期間變更,請先決定結果應代表哪個時點的值。
在 Mac 上執行完整的本機比較
請使用 macOS 13 或更新版本,以及已安裝 Swift 6 或更新版本的工具鏈。此處使用的時間長度型 Task.sleep(for:) 多載,文件記載支援 macOS 13 及更新版本。它會暫停工作,而不阻塞底層執行緒;若等待結束前發生取消,則會擲出 CancellationError。sleep(for:tolerance:clock:)
將以下完整程式儲存為 CaptureDemo.swift。計算器、工作閉包與示範都在 @MainActor 上執行,因此其狀態存取會依序進行。Swift 文件說明了類別層級的主執行緒隔離,以及此處使用的明確閉包標註。Concurrency
import Foundation
enum CapturePolicy: String {
case strong, weak, snapshot
}
@MainActor
func formatTotal(_ samples: [Int]) -> String {
let total = samples.reduce(0, +)
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.usesGroupingSeparator = false
formatter.maximumFractionDigits = 0
guard let text = formatter.string(from: NSNumber(value: total)) else {
return "Formatting unavailable"
}
return "Total: \(text)"
}
@MainActor
final class Calculator {
var samples = [4, 8, 12]
func start(_ policy: CapturePolicy) -> Task<String, Error> {
switch policy {
case .strong:
return Task { @MainActor [self] in
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
return formatTotal(self.samples)
}
case .weak:
return Task { @MainActor [weak self] in
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
guard let self else {
return "Owner unavailable"
}
return formatTotal(self.samples)
}
case .snapshot:
return Task { @MainActor [samples = self.samples] in
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
return formatTotal(samples)
}
}
}
}
@main
struct CaptureDemo {
@MainActor
static func demonstrate(
_ policy: CapturePolicy,
cancel: Bool = false
) async {
var owner: Calculator? = Calculator()
weak var probe = owner
let task = owner!.start(policy)
owner = nil
let label = cancel ? "\(policy.rawValue), cancelled" : policy.rawValue
print("\(label) retains owner: \(probe != nil)")
if cancel {
task.cancel()
}
do {
let result = try await task.value
print(result)
} catch is CancellationError {
print("Cancelled")
} catch {
print("Calculation failed: \(error)")
}
}
@MainActor
static func main() async {
await demonstrate(.strong)
await demonstrate(.weak)
await demonstrate(.snapshot)
await demonstrate(.snapshot, cancel: true)
}
}
算術部分使用標準程式庫的 reduce(0, +),將整數合併成一個總和。Foundation 的 NumberFormatter 會將總和轉成顯示文字;其 string(from:) 方法會回傳可選字串,因此程式會明確回報格式化失敗。固定範例的總和為 24。reduce(::)、NumberFormatter、string(from:)
在 Terminal 中切換到檔案所在資料夾,並執行以下命令。目標指定 macOS 13,適用於 Mac 的處理器架構。-parse-as-library 選項允許檔案使用明確的 @main 進入點。Swift 編譯器接受原始碼檔案與命令列選項;swiftc -help 會列出工具鏈中可用的選項。Swift command-line manual
swift --version
swiftc -parse-as-library -swift-version 6 -target "$(uname -m)-apple-macos13.0" CaptureDemo.swift -o CaptureDemo
./CaptureDemo
預期的應用程式輸出如下。這是根據捕獲與取消規則推導而來;本文件的研究環境沒有 Swift 編譯器,因此程式經過檢視,但未在此執行。
strong retains owner: true
Total: 24
weak retains owner: false
Owner unavailable
snapshot retains owner: false
Total: 24
snapshot, cancelled retains owner: false
Cancelled
弱 probe 會觀察擁有權,但不會新增另一個強參照。每個示範都會在等待工作前清除自己的強參照。由於示範與工作閉包使用相同的 actor,新建立的閉包無法插入這段同步設定流程。如此一來,比較結果不必仰賴猜測工作何時啟動。Concurrency
為什麼弱參照檢查的位置很重要?
在弱參照分支中,guard let self 出現在延遲之後。因此工作可以等待,而不會強烈持有擁有者。如果擁有者已消失,明確回傳的 Owner unavailable 結果就會結束該計算。
將解包移到延遲之前會改變生命週期。以下是用來取代弱參照分支閉包主體的示意片段;它不是獨立程式:
// Inside the existing [weak self] task closure:
guard let self else {
return "Owner unavailable"
}
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
return formatTotal(self.samples)
在此,成功的可選值綁定會建立一個強區域參照,並在暫停之後再次使用。因此,一旦綁定成功,擁有者就會在等待期間持續可用。這符合 Swift 的強參照規則:弱捕獲決定閉包一開始如何取得擁有者,而解包後的區域變數則決定後續程式碼如何持有它。Automatic Reference Counting
若契約是「等待時不保留擁有者,之後僅在它仍存在時才計算」,請將解包留在等待之後,並立即執行簡短計算。解包後若還有其他暫停點,就需要再次決定生命週期。
unowned 捕獲要求每次存取時,被參照的實例都仍然有效。它無法提供此範例所需的可選缺席檢查;此處刻意允許擁有者在延遲期間被釋放。Automatic Reference Counting
釋放工作控制代碼會停止計算嗎?
即使丟棄工作控制代碼,工作仍會繼續執行。保留控制代碼可讓你等待其結果或要求取消。Apple 也說明,已完成的工作會立即釋放其操作閉包及該閉包持有的參照。因此,有限時間的強捕獲會讓擁有者在工作期間持續存活,但不一定會形成永久的參照循環。Task
取消與擁有權是不同的契約。最後一個示範透過控制代碼要求取消。會擲出錯誤的休眠操作與後續的 Task.checkCancellation() 提供了操作可以停止的時機,而呼叫端會回報該結果。Swift 的取消機制採合作式:提出取消要求不會強行中斷任意計算程式碼。Task
如果取消應該阻止產生結果,請勿將會擲出錯誤的休眠操作改成 try?,然後不加判斷地繼續執行。忽略該錯誤會讓程式繼續進入計算,除非另有取消檢查予以阻止。同樣地,將擁有者變數設為 nil 並不是取消要求。
對桌面預覽而言,這些區別帶來三項實用控制:擁有者是否維持配置、計算使用哪些輸入值,以及操作是否應停止。請明確選擇每一項,不要期待弱捕獲能一併處理所有問題。
桌面預覽應如何調整?
若待處理的預覽只有在擁有者仍可用時才有價值,請使用弱參照版本。若選定的合成輸入應獨立產生結果,請使用快照版本。若完成操作確實需要擁有者,請使用強參照版本。
物件生命週期也不同於介面是否可見。視窗關閉本身無法證明所有對計算器的強參照都已移除。若應用程式應在預覽關閉時停止待處理工作,請將該應用程式事件連結到保留的工作控制代碼之 cancel() 方法;計算本身仍須配合取消機制。Task
讓此範例的工作量保持小型。主執行緒隔離讓狀態存取簡單明瞭,等待也會暫停,但同步加總仍在該 actor 上執行。Swift 並行處理指南說明,主執行緒 actor 也負責處理介面工作。若計算量大得多,就需要採用不同的執行設計,並將適當的值傳給工作執行緒。Concurrency
對這項本機計算而言,判斷方式很簡單:在延遲操作執行前移除呼叫端的參照,然後觀察結果。強捕獲會保留擁有者;弱捕獲允許明確回報擁有者不可用;整數陣列快照則只保留完成計算所需的輸入值。
來源
- Automatic Reference Counting — Swift.org
- Expressions — Swift.org
- Concurrency — Swift.org
- Task — Apple Developer
- sleep(for:tolerance:clock:) — Apple Developer
- reduce(::) — Apple Developer
- NumberFormatter — Apple Developer
- string(from:) — Apple Developer
- Swift command-line manual — Swift project