Swift 6.2 並行實務:預設使用 MainActor,刻意時才離開
Swift 6.0 讓資料競爭成為編譯錯誤,而在過去一年裡,所有人都為此付出了代價。嚴格並行檢查器把尋常的 UI 程式碼變成一整面牆的 Sendable 違規,以及「main actor-isolated property can not be referenced from a nonisolated context」之類的錯誤。診斷本身是正確的(那些程式碼確實有可能競爭),但錯誤的數量淹沒了訊號,導致許多團隊不是繼續停留在 Swift 5 模式,就是到處灑上 @MainActor,直到錯誤安靜下來為止。
Swift 6.2 改的是預設值,而非規則。資料競爭安全的保證並未改變;改變的是編譯器的起點。採用新的預設值之後,那面牆大多會消失,因為編譯器現在會假設您的 app 本來就成立的事實:大部分程式碼都在 main actor 上執行,而您只在指名的特定地方刻意離開它。這正是我在 941 系列 app 中採用的模型1,而它也是更大演進脈絡中的一環——2026 年 Swift 新功能總覽便循著隨後而來的 6.3 與 6.4 提案,梳理了這整條軸線。以下說明它如何運作,以及啟用之後仍會咬人的六個錯誤。
TL;DR
- SE-0466 讓您能將整個模組預設為
@MainActor。 設定SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor,從此不必為每個 view、model 與 view-model 加上標註2。 - SE-0461 讓 nonisolated 的
async函式預設在呼叫端的 actor 上執行(nonisolated(nonsending)),因此呼叫 async 程式碼不再強制發生 actor 跳轉,連帶而來的跨邊界Sendable錯誤也隨之消失3。 @concurrent是逃生艙口。 將函式標記為@concurrent,即可刻意且明確地把繁重的 CPU 工作(解碼、影像處理)推到背景執行緒上4。- Xcode 26 會為新專案開啟這兩項預設。總開關是
SWIFT_APPROACHABLE_CONCURRENCY = YES5。 - 這個模型翻轉了負擔:您不必再證明每一行在 main 以外執行都安全,而是把所有東西都留在 main 上,只證明您離開的那少數幾處。
- 切換之後仍存留六個具體錯誤。一旦看出模式,這六個全都只需一行修正。
翻轉,以及它為何重要
舊模型(Swift 6.0):程式碼在您隔離它之前都是 nonisolated。每個碰到 UI 狀態的型別都需要 @MainActor,每個跨隔離邊界的 async 呼叫都需要 Sendable 一致性,而編譯器會標出每一處缺口。對於一個 95% 程式碼早已在主執行緒上跑的 app 來說,您把時間花在標註那 95%,只為描述一個從來無人質疑的事實。
新模型(Swift 6.2):程式碼在您離開它之前都在 main actor 上。SE-0466 讓您可以把 main-actor 隔離宣告為模組預設,於是一個 view、它的 model 與它的輔助型別,全都無需任何標註就是 @MainActor2。SE-0461 接著移除了第二項稅負:nonisolated 的 async 函式現在會在呼叫它的那個 actor 上執行,而非跳轉到全域 executor,因此 await 它不會把您拖過隔離邊界,也不會要求作用域內的所有東西都是 Sendable3。
這個心智模型正好對應 UI app 實際的行為方式。預設在主執行緒並非妥協;它是一個以 view 為狀態的 app 的本質。並行成為您主動伸手去取、指名且受限的例外,而非您必須在每一行上防備的環境條件。編譯器的工作從「證明這在並行執行下是安全的」翻轉為「您說這是並行執行的,那就證明它安全」,而第二個問題只會在少得多的地方被問到。
Swift 核心團隊也是這麼說的。在 WWDC 2026 Swift Group Lab 中,Holly 描述道,若以今日的眼光設計並行模型,nonisolated async 函式從第一天起就會在呼叫端的脈絡中執行;她轉述自己曾長期堅持全域並行池模型,後來被真實專案中的問題說服而改觀。Doug 補充說,舊的預設把太多型別推向了 Sendable。(兩段引述皆轉述自本地謄錄的實驗室錄音;Apple 並未為該場次發布字幕。)9
開啟它
兩項建置設定,皆已是 Xcode 26 新專案的預設,在既有專案上也都值得明確設定5:
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
SWIFT_APPROACHABLE_CONCURRENCY = YES
第一項是 SE-0466:模組預設為 main-actor 隔離。第二項是啟用 approachable-concurrency 功能集的總開關,其中包含 SE-0461 的「呼叫端執行」行為。在 Swift 套件中,您會透過 swiftSettings 搭配對應的 upcoming-feature 旗標來設定相同的預設,而非使用 Xcode 建置設定6。
在既有專案上同時開啟這兩項,錯誤數量會大幅下降,因為檢查器先前標記的內容,大多是它過去無法假設為主執行緒的主執行緒程式碼。剩下的是一份簡短的真實邊界案例清單。它們值得逐一記住名稱,因為每一個都是您的程式碼確實離開 main actor 之處,而修正之道就是精確地把這件事說出來。
切換之後仍存留的六個錯誤
以下是在 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 之下仍會出現的嚴格並行錯誤,取自將 941 系列 app 遷移至此模型的過程1。每一個都是真實的邊界,而非誤判,這也是為何修正之道是精確的標註,而非抑制。
1. Sendable 型別上的純函式。 一個 Sendable enum(URL 建構器、格式化器)上的純方法,在模組預設下會繼承 main-actor 隔離,接著在從 nonisolated 脈絡呼叫時報錯:「Call to main actor-isolated instance method in a synchronous nonisolated context.」 該方法不碰任何狀態,因此把它隔離到 main actor 是錯的。將它標記為 nonisolated7:
nonisolated func searchURL(for query: String) -> URL? { ... }
2. 預設參數中的單例 static。 static let shared = Foo() 在預設下是 main-actor 隔離的,但預設參數的值是在呼叫端的脈絡中求值,而那通常是 nonisolated 的:「Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context.」 將該 static 標記為 nonisolated。如果該型別是 Sendable(或因為您自行守護其狀態而為 @unchecked Sendable),就不需要任何 unsafe 限定詞:
final class KeychainProxySecretStore: @unchecked Sendable {
nonisolated static let shared = KeychainProxySecretStore()
}
3. 不可變的基本常數。 同樣的預設參數問題也會打到一個普通常數:一個從 nonisolated 預設引數所參照的 static let defaultInterval。修正方式完全相同,而這個常數共享起來顯然安全:
nonisolated static let defaultInterval: TimeInterval = 15 * 60
4. 讀取已捕捉 self 的 Task 主體。 外層 closure 捕捉了 [weak self];在其內部,一個 Task { @MainActor in self?.foo() } 讀取了那個被捕捉的 optional:「Reference to captured var ‘self’ in concurrently-executing code.」 這個 Task 從外圍作用域並行地讀取一個 var 繫結,這正是競爭所在。在 Task 邊界重新捕捉 self,讓 Task 擁有一個不可變的繫結:
NotificationCenter.default.addObserver(...) { [weak self] _ in
Task { @MainActor [weak self] in
self?.value = next
}
}
5. 讀取 main-actor 狀態的 KVO 回呼。 一個 webView.observe(\.canGoBack) { wv, _ in ... } 回呼是 @Sendable 的,因此屬於 nonisolated,但 WKWebView.canGoBack 是 main-actor 隔離的:「Main actor-isolated property ‘canGoBack’ can not be referenced from a Sendable closure.」 KVO 會在改動該值的那條執行緒上同步遞送,而 WKWebView 的導覽狀態只在主執行緒上改動,因此這個讀取是健全的。用 MainActor.assumeIsolated 來斷言它,這能完全省去 Task 跳轉並保持同步8:
let pushNav: @Sendable (WKWebView?) -> Void = { [weak self] webView in
MainActor.assumeIsolated {
guard let self else { return }
// safe to read webView?.canGoBack synchronously
}
}
assumeIsolated 是對編譯器許下的承諾,而非提問。只在執行期不變式確實成立之處(一個有文件記載的主執行緒回呼)才使用它,因為錯誤的承諾換來的是崩潰,而非警告。
6. 不該留在 main 上的繁重工作。 這一個檢查器不會標記,而它卻是您最需要自己抓出來的。在 main-actor 預設下,一個同步且受 CPU 限制的方法(解碼大型 JSON 酬載、縮放影像)會在 main actor 上執行並讓您的 UI 卡頓。預設把您留在 main 上;@concurrent 則是您刻意離開的方式4:
@concurrent
func decodeLargePayload(_ data: Data) async throws -> Report {
try JSONDecoder().decode(Report.self, from: data)
}
@concurrent 會把函式卸載到全域 executor,且依設計與 @MainActor、自訂全域 actor 以及 nonisolated(nonsending) 互斥:一個函式要嘛在呼叫端所在之處執行,要嘛刻意地離開它,絕不含糊其詞4。新模型所要求的紀律,全都落在這個模式裡。凡是碰到 UI 的東西都留在 main 上,只在工作可量測地需要背景執行緒時,才伸手去取 @concurrent。
當這些預設不適合您時
main-actor 預設很適合 app:SwiftUI 與 UIKit 程式碼壓倒性地都在主執行緒上,預設正好對應現實。但它在兩種情況下就沒那麼合適,假裝不是如此只會浪費您的時間。
- 沒有 UI 的函式庫或框架 target。 一個網路層、一個解析器,或一個資料引擎,沒有理由預設為 main actor,這樣做會迫使幾乎所有東西都要加上
@concurrent或nonisolated。請讓這類 target 維持SWIFT_DEFAULT_ACTOR_ISOLATION未設定,並以舊有的反向方式刻意隔離。 - 以 actor 為主的並行系統。 如果您的設計確實同時平行執行許多事(一條真正的管線,而非帶有幾個背景任務的 app),main-actor 預設會跟您作對。您要的是明確的 actor 與 nonisolated 程式碼,而 SE-0466 的預設正是錯誤的起點。
不過對一個 app 而言,抉擇很簡單:把這兩項設定都開啟,讓錯誤數量崩塌,並把剩下的那寥寥幾個視為一張地圖,精確標出您的程式碼在何處離開了主執行緒。那張地圖值得擁有。舊模型給您一千個警告卻沒有地圖;新模型給您六個誠實的邊界,以及一個終於對應 app 實際執行方式的預設。
最後談一點關於雜訊與訊號的提醒。在 Xcode 重建索引時(尤其是剛重新產生專案之後),SourceKit 會在編輯器內顯示跨檔案的索引錯誤(「Cannot find type X in scope」、「No such module」)。那些是索引產物,而非並行錯誤。只要 xcodebuild 回報 BUILD SUCCEEDED,並行模型就已滿足,編輯器只是還在追趕1。追逐索引幽靈,是在一個其實早已成功的遷移上浪費一整個下午最快的方法。
FAQ
我要如何在 Swift 6.2 中啟用 main-actor 預設的並行?
設定兩項建置設定,它們在 Xcode 26 新專案中都預設開啟:SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 與 SWIFT_APPROACHABLE_CONCURRENCY = YES5。在 Swift 套件中,您會改透過 swiftSettings 設定對應的 upcoming-feature 旗標,而非使用 Xcode 建置設定6。
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 實際上改變了什麼?
它讓模組的程式碼預設為 main-actor 隔離(SE-0466),於是一個 view、它的 model 與它的輔助型別,全都無需標註就是 @MainActor2。並行成為您主動伸手去取的指名例外,而非您必須在每一行上加以標註防備的環境條件。
什麼是 @concurrent,何時該使用它?
@concurrent 會刻意地把一個 async 函式卸載到全域 executor,使其離開呼叫端的 actor4。在 main-actor 預設下,您用它來處理可量測地需要背景執行緒的工作(解碼大型酬載、縮放影像);它與 @MainActor 互斥,因此一個函式要嘛在呼叫端所在之處執行,要嘛刻意地離開它。
我為什麼會收到「Main actor-isolated … can not be referenced from a nonisolated context」?
在 main-actor 預設下,純函式、單例 static 與常數會繼承 main-actor 隔離,接著在從 nonisolated 脈絡(例如預設參數求值)呼叫時報錯。修正之道是在該成員上加上精確的 nonisolated 標註,而非抑制;由於該值不碰任何 actor 狀態,共享起來是安全的7。
我該為函式庫或框架 target 使用 main-actor 預設嗎?
不該。一個網路層、解析器或資料引擎沒有理由預設為 main actor,這樣做會迫使幾乎所有東西都要加上 @concurrent 或 nonisolated。請讓非 UI 的 target 維持 SWIFT_DEFAULT_ACTOR_ISOLATION 未設定,並以舊有的反向方式刻意隔離。
在 Swift 6.2 中,await 一個 nonisolated async 函式仍會跳轉 actor 嗎?
不會。SE-0461 讓 nonisolated 的 async 函式在呼叫它的那個 actor 上執行,而非跳轉到全域 executor,因此 await 它不再把您拖過隔離邊界,也不再要求作用域內的所有東西都是 Sendable3。
-
作者橫跨 941 系列 iOS app(Ki、Return、Get Bananas)的正式環境程式碼,全部以
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor與SWIFT_APPROACHABLE_CONCURRENCY = YES出貨。下方的六個錯誤模式與修正,正是 Ki 1.0.0 完整的嚴格並行清理集。 ↩↩↩ -
Swift Evolution,SE-0466: Control default actor isolation inference。讓模組預設為
@MainActor隔離,於是 UI 與 app target 都在 main actor 上執行,除非程式碼透過@concurrent或明確的 actor 選擇退出。 ↩↩↩ -
Swift Evolution,SE-0461: Run nonisolated async functions on the caller’s actor by default。nonisolated 的
async函式預設為nonisolated(nonsending),在呼叫端的 actor 上執行,而非跳轉到全域 executor,這移除了隨跳轉而來的跨邊界Sendable要求。 ↩↩↩ -
Swift Evolution,SE-0461 引入了
@concurrent屬性,讓函式選擇在全域 executor(背景執行緒)上執行。@concurrent與nonisolated(nonsending)是 nonisolated async 函式的兩種對立隔離模式:一個函式要嘛在呼叫端所在之處執行,要嘛刻意地離開它。@concurrent無法與@MainActor或自訂全域 actor 結合。 ↩↩↩↩ -
SWIFT_APPROACHABLE_CONCURRENCY是啟用 approachable-concurrency upcoming 功能(含 SE-0461 行為)的 Xcode 總開關建置設定,而SWIFT_DEFAULT_ACTOR_ISOLATION則選擇模組的預設隔離。Xcode 26 新專案會以 main-actor 預設同時啟用兩者。記載於 Donny Wals,“Exploring concurrency changes in Swift 6.2”,以及 Paul Hudson,“What’s new in Swift 6.2”,兩者皆與底層提案 SE-0461 及 SE-0466 交叉比對過。 ↩↩↩ -
Swift,Swift Concurrency Migration Guide,「Enabling Complete Concurrency Checking」與語言模式設定。在 Swift 套件中,預設隔離與 approachable-concurrency 功能是透過
swiftSettings的 upcoming-feature 旗標設定,而非 Xcode 建置設定。 ↩↩ -
Swift,Migration Guide: global actor isolation and
nonisolated。一個@MainActor型別的方法會繼承 main-actor 隔離;nonisolated讓方法選擇退出,這對於不碰任何隔離狀態的純函式而言是正確的。 ↩↩ -
Apple Developer,
MainActor.assumeIsolated(_:)。斷言當前執行已在 main actor 上,並同步執行該 closure 而不發生 actor 跳轉。若該不變式不成立,該斷言會在執行期觸發 trap,因此它只在呼叫端保證位於主執行緒之處才有效。 ↩ -
Apple,WWDC 2026 Swift Group Lab。歸於 Swift 核心團隊 Holly 與 Doug 的言論,係轉述自本地謄錄的錄音;Apple 並未為該場次發布官方逐字稿或字幕,因此用詞為近似,而非逐字引述。 ↩