Control default actor isolation inference
01 何が問題だったのか
Swift 6の言語モードでは、明示的に isolation が指定されていない宣言はデフォルトで nonisolated として扱われます。これは「並行性があるかもしれない」という前提に基づいた設計であり、どの isolation domain からも利用できる状態になります。
しかし、現実には多くのSwiftプログラムは事実上シングルスレッドで動作しています。アプリケーション、コマンドラインツール、スクリプトの多くは MainActor 上で起動し、Task などを明示的に使わない限りずっと MainActor 上で動き続けます。並行性が一切使われないのであれば、データ競合が発生する余地はなく、コンパイラが出す並行性に関する診断はすべて偽陽性になってしまいます。
この「デフォルトで nonisolated」という方針により、シングルスレッドのコードを書いているだけなのに、以下のような場面で false-positive な診断が大量に発生していました。
- グローバル変数・静的変数の宣言
@MainActor-isolated な型を、isolation 指定のないプロトコルに適合させるとき- クラスの
deinit @MainActor-isolated なサブクラスで、isolation 指定のないスーパークラスのメソッドをオーバーライドするとき- プラットフォームSDKが提供する
@MainActor-isolated な関数の呼び出し
例えば、UIアプリを書き始めたばかりの初心者が、単純にグローバル変数を定義したり、標準的なプロトコル適合を書いたりするだけで、本来考えなくてよいはずの並行性の診断に遭遇してしまいます。並行性を一度も使っていないのに、並行性の概念を先に理解しないと diagnostics を解消できないという状況は、Swiftの progressive disclosure(段階的な開示)の理念に反しており、特に学習者にとって言語の敷居を大きく上げる原因になっていました。
シングルスレッドのコードを「シングルスレッドのコードとして」自然に書けるようにすることが、求められていました。
02 どのように解決されるのか
モジュール単位で「デフォルトの isolation」を MainActor に切り替えられるオプトイン機構が導入されました。これにより、シングルスレッド前提のコードは最初から @MainActor-isolated として扱われ、並行性の前提による偽陽性の診断が出なくなります。
コンパイラフラグ
-default-isolation フラグで、モジュール全体のデフォルト isolation を指定します。指定できる値は MainActor と nonisolated の2つだけで、両方を同時に指定するとエラーになります。フラグを指定しなかった場合のデフォルトは、従来通り nonisolated です。
swiftc -default-isolation MainActor ...
Swift Package Manager
Swift Packageでは、ターゲットごとに SwiftSetting.defaultIsolation で設定できます。
// Package.swift
.target(
name: "MyApp",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
引数に渡せるのは MainActor.self または nil のみで、nil は nonisolated を意味します。これは #isolation が返す値と同じ表現形式に揃えられています。
推論ルール
-default-isolation MainActor を指定すると、isolation 指定のない宣言は暗黙的に @MainActor-isolated として推論されます。ただし、以下のケースにはこのデフォルトは適用されません。
- 明示的に isolation が指定されている宣言
- スーパークラス、オーバーライド元、適合しているプロトコル、メンバー伝播などから isolation が推論される宣言
actor型の内部にあるすべての宣言(static変数、メソッド、イニシャライザ、デイニシャライザを含む)typealias、import、enumのcase、個別のアクセサなど、そもそもグローバルアクターに isolate できない宣言SendableMetatypeを継承するプロトコルに直接適合する型(SendableやCodingKeyなどがこれに該当)nonisolatedな型の内部にネストされた型
コード例で示すと、-default-isolation MainActor でビルドしたときのコメント部分が推論される isolation です。
// @MainActor
func f() {}
// @MainActor
class C {
// @MainActor
init() {}
// @MainActor
deinit {}
// @MainActor
struct Nested {}
// @MainActor
static var value = 10
}
@globalActor
actor MyActor {
// nonisolated(actor内はデフォルト推論の対象外)
init() {}
// nonisolated
deinit {}
// nonisolated
static let shared = MyActor()
}
@MyActor
protocol P {}
// @MyActor(プロトコル適合から推論される)
struct S: P {
// @MyActor
func f() {}
}
nonisolated protocol Q: Sendable {}
// nonisolated(SendableMetatypeを継承するプロトコルに直接適合)
struct S2: Q {
// nonisolated(nonisolated型の内部にネスト)
struct Inner {}
// @MyActor(プロトコル適合が優先される)
struct IsolatedInner: P {}
}
// @MainActor
struct S3 {}
// S3 自体の宣言は @MainActor のまま(extensionでの適合は主要定義に該当しない)
extension S3: Q {}
クロージャ
クロージャの推論ルールそのものは変わりません。@Sendable でないクロージャや Task.init に渡すクロージャは、これまで通り囲むコンテキストの isolation を引き継ぎます。結果として、囲むコンテキストが @MainActor 推論の対象であれば、その中のクロージャも @MainActor-isolated になります。
// -default-isolation MainActor でビルド
// @MainActor
func f() {
Task { // @MainActor in
// ...
}
Task.detached { // nonisolated in
// ...
}
}
nonisolated func g() {
Task { // nonisolated in
// ...
}
}
並行性を使いたいとき
モジュール全体を MainActor デフォルトにしていても、並行性が必要な箇所では SE-0449 で任意の宣言に付けられるようになった nonisolated を明示的に指定することで、従来通り nonisolated なコードを書けます。
// -default-isolation MainActor でビルド
nonisolated func compute(_ x: Int) -> Int {
// このスコープは nonisolated
return x * 2
}
nonisolated struct Worker {
func run() async { /* ... */ }
}
この機構によって、アプリやスクリプトのような「シングルスレッドが自然」なモジュールは並行性のことを気にせず書き始められ、並行性が必要になった箇所だけ nonisolated を付ける、という progressive disclosure な書き味が実現されます。
今後の展望
-default-isolation は現状モジュール単位の設定ですが、将来的にはファイル単位で設定を切り替えられるようにすることが検討されています。#pragma に近いコンパイラディレクティブを導入し、ファイル内で個別に isolation のデフォルトや診断挙動を指定できるようにする、というアイデアが別途議論されています(本提案では採用されていません)。