如果團隊開啟過 Swift 6 模式,應該記得那一刻:原本運作正常的專案突然湧出數十、數百個並行處理錯誤。
錯誤訊息的主角大多只有一個:Sendable。
Concurrency 系列第三篇整理最後這塊拼圖,以及 Swift 6 的 strict concurrency。
Sendable 提出的問題 — 這個值可以跨越邊界嗎
在 actor 篇中,我們將隔離整理為「序列執行脈絡」:actor 內、MainActor 上,以及兩者皆非的協作式執行緒池。
但值會頻繁跨越這些邊界。它們會成為 actor 方法的引數、作為回傳值離開,並被 Task 閉包捕捉。
危險就在這裡。隔離保護的是 actor 自身的狀態,但如果跨界的值是可變參考型別呢?
同一個實例可能同時被兩個隔離脈絡持有,原本由 actor 阻擋的資料競爭,會透過帶入的物件復活。就像邊境檢查很嚴格,卻不檢查帶入物品。
Sendable 就是這套檢查標準。它是沒有必要方法的標記協定,意義只有一個:
此型別的值跨越隔離邊界後仍可安全地同時使用。
哪些型別安全?直覺上很簡單。
值型別跨越時會被複製,因此安全(前提是所有儲存屬性都是 Sendable)。Int、String,以及只由 Sendable 屬性組成的 struct 和 enum 都屬於此類。多數情況下編譯器會自動認可。
actor 也安全,因為它內建了自我保護。
不可變類別(final 且只有 let 屬性)也安全。既然不能變更,就不會有競爭。
真正危險的只有一種:具有可變狀態的類別。值型別優先篇整理的「共享+可變=危險」,正是 Sendable 的判定標準。
函式型別另有 @Sendable 標記。Task { }中的閉包就是典型的 @Sendable 閉包;加上此標記的閉包不能捕捉 non-Sendable 值。
閉包篇提過,捕捉可能成為跨越隔離邊界的走私通道,因此語言會檢查通道本身。
strict concurrency — 將警告變成錯誤,將規範變成檢查
這些規則在 Swift 6 之前就存在,但預設基本上保持沉默。
Swift 6 語言模式的核心,就是開啟全部檢查並將問題升級為錯誤,也就是 strict concurrency。在 complete checking 下,non-Sendable 值跨越隔離邊界的每個位置都會成為編譯錯誤。
錯誤大量出現,不是因為程式碼突然變差,而是原本潛藏的競爭現在才被看見。
這就像導入 Optional 時,所有「可能是 nil 的地方」都被揭露。當時需要 nil 檢查,如今則是把並行處理假設移入型別系統的過渡期。
幸好編譯器也比想像中更聰明。Swift 6 加入的區域導向隔離分析(Swift Evolution 提案 SE-0414,region-based isolation)就是例子。
即使是 non-Sendable 值,只要能證明「傳送方不會再碰它」,也允許移動。
因為所有權已移轉的值不可能製造競爭。因此,許多理論上應該報錯的程式碼實際上仍能通過。
sending 參數標記也遵循相同方向。總之,規則正從「一律禁止」變得更精準,改為「證明安全後允許」。
實戰遷移 — 依錯誤類型處理
大量錯誤大多會收斂成幾種模式。以下整理各類型的標準處方。
類型 1。自製模型是 non-Sendable。 這是最常見、也最健康的錯誤。第一優先是改成 struct。
如果不需要參考身分的資料模型原本宣告成 class,這正是移到值型別的理由。
如果必須是 class,就用 final + let 使其不可變並採用 Sendable。如果必須可變,代表狀態需要擁有者,因此應考慮升級為 actor。
類型 2。全域變數・static var。 過去提醒「static var 實質上就是全域狀態」的所有位置都會變成錯誤。
如果真的需要全域狀態,就宣告隔離。UI 相關加上 @MainActor,否則用 actor 包裝或改成不可變的 let。
類型 3。Delegate・callback 類別卡在邊界。 這常出現在與 UIKit 時代 API 交會的位置。
若該型別實質上只供主執行緒使用,多數情況宣告 @MainActor 就是答案。把「這個類別本來就只在主執行緒使用」的默認事實改成明確宣告。
類型 4。實際安全,但編譯器不知道。 例如內部以鎖保護的類別、C 函式庫包裝器。
此時的出口是 @unchecked Sendable。它宣告「安全性由我保證,請關閉檢查」,但名稱中的 unchecked 已提醒這是 unsafe 類工具。
依照哲學篇第一篇所說的明確出口原則,應在註解中留下保證依據(哪把鎖保護什麼),並限制在最小範圍使用。
如果為了遷移方便開始用 unchecked 填滿錯誤,最後只會得到關閉檢查、卻掛著 Swift 6 標誌的程式碼。
策略上不必一次全部開啟。語言模式可以按模組選擇。
標準做法是由下而上:先將相依性少的末端模組(工具與模型)升級到 Swift 6 模式,最後才升級 App target。
也可以透過 Xcode 的 upcoming feature 旗標先提高檢查等級,觀察警告,作為準備階段。
看懂方向 — 為什麼要讓我們吃這些苦
遷移的痛苦確實存在,因此有必要準確理解付出成本的理由。
Swift 6 的承諾是:能編譯就代表沒有資料競爭。無法重現的間歇性崩潰,以及只在發布後發生的時序錯誤,這整類問題都會在編譯期消失。
這和記憶體安全(Optional、ARC — Automatic Reference Counting、自動參考計數)走過的路相同。並行處理安全性正從「寫得好就行」轉變為「由語言保證」。
這個方向也是哲學系列軌跡的延伸:將容易出錯的規範提升到型別中,在有成本的地方要求明確標記(@unchecked、sending),再以漸進式採用(模組級語言模式)吸收過渡期摩擦。
Swift Evolution 篇提到的流程仍持續加入基本隔離選項等緩衝機制來降低摩擦。現在遇到的警告,正是這場轉換的中途。
總結
- Sendable 是「型別跨越隔離邊界後仍能安全同時使用」的標記。值型別、actor、不可變類別安全;可變類別就是全部的危險來源。
- @Sendable 閉包會檢查捕捉,防止閉包成為競爭的走私通道。
- Swift 6 strict concurrency 將這些檢查升級為錯誤。錯誤爆發不是程式碼變差,而是潛在競爭浮現。
- 處方優先順序:值型別化 → 不可變化 → 升級為 actor → 宣告隔離(@MainActor)→ 最後使用附上依據的 @unchecked Sendable。
- 從末端模組由下而上遷移,語言模式按模組逐步提升。
下一篇是 Concurrency 系列的收尾:結構化並行處理。內容涵蓋 Task、async let、TaskGroup 組成的工作樹,以及取消(cancellation)如何沿著這棵樹傳播。

![[Swift 進階 #3] Sendable 與 Swift 6 並行處理錯誤遷移 封面圖](/assets/images/posts/053e9ee5-0023-4c58-838f-c7d6d31cf70e/swift-sendable-strict-concurrency-1.jpg)