Allow nonisolated to prevent global actor inference
01 何が問題だったのか
グローバルアクターの isolation は、さまざまな経路で暗黙に推論されます。たとえば、@MainActor を付けたプロトコルに適合する型は、自分で書いていなくても暗黙に @MainActor-isolated になります。
@MainActor
protocol GloballyIsolated {}
struct S: GloballyIsolated {} // 暗黙に globally-isolated
この例のように 1 段だけなら挙動は単純ですが、適合リストが長くなったり、プロトコルのリファイン関係やスーパークラス経由で連鎖的に推論が起きたりすると、「どこから isolation が来ているのか」を読み手が追いかけるのが難しくなります。
一方で、プロトコルがグローバルアクターに isolated でも、それに適合する型は nonisolated のままにしておきたい、というケースも珍しくありません。ところが、型全体のグローバルアクター推論を止める方法が、これまで用意されていませんでした。個々のメソッドに nonisolated を付けていくことはできますが、型レベルでまとめて opt out する手段がないのです。
これまで推論を「遮断 (cut-off)」するには、不格好な回避策に頼るしかありませんでした。1 つ目の方法は、適合をエクステンションで書いたうえで、そのエクステンション内の要件をすべて nonisolated で明示する、というものです。
@MainActor
protocol P {
var x: Int { get }
}
struct S {}
extension S: P {
nonisolated var x: Int {
get { 1 }
}
nonisolated func test() {
print(x)
}
}
この書き方では、S はグローバルアクターに isolated なプロトコル P に適合しつつも自身は nonisolated でいられますが、要件ごとに nonisolated を書き続けなければならない負担があります。
2 つ目は、プロトコル自体のグローバルアクター推論を止めたいときの回避策です。コンパイラは、グローバルアクターの推論元が複数あって互いに食い違う場合、「どれにも決められない」としてグローバルアクターを推論しません。これを逆手にとり、別のグローバルアクターに isolated なダミーのプロトコルを用意し、両方をリファインすることで推論を打ち消す、というテクニックが知られていました。
@MainActor
protocol GloballyIsolated {}
@FakeGlobalActor
protocol RemoveGlobalActor {}
protocol RefinedProtocol: GloballyIsolated, RemoveGlobalActor {} // 'RefinedProtocol' は non-isolated
RefinedProtocol は @MainActor と @FakeGlobalActor という競合する推論元を持つため、結果としてどのグローバルアクターにも isolated にならずに済みますが、このためだけにダミーのアクターとプロトコルを用意する必要があり、明らかに本筋から外れた書き方です。
加えて、stored property に nonisolated を書けるかどうかのルールも部分的で、「型としては Sendable な値型なのに、プロトコル要件の isolation がそのまま witness に残ってしまう」といった使い勝手の悪さが残っていました。
02 どのように解決されるのか
nonisolated を型・プロトコル・エクステンションの宣言にも書けるようにし、その宣言についてはグローバルアクターの推論を打ち切ることができるようにします。併せて、stored property に nonisolated を書けるルールも拡張し、使い勝手を改善します。
型・プロトコル・エクステンションへの nonisolated
class / struct / enum / protocol / extension の宣言に nonisolated を書くと、その宣言はグローバルアクターの推論の対象外になります。
nonisolated struct S: GloballyIsolated, NonIsolatedProto {} // 'S' は 'GloballyIsolated' から isolation を継承しない
プロトコルに付けた場合も同様で、リファイン元のグローバルアクター isolation を引き継ぎません。要件自体はそのまま残りますが、isolated ではなくなります。
nonisolated protocol Refined: GloballyIsolated {}
struct A: Refined {
var x: NonSendable
nonisolated func printX() {
print(x) // OK, 'x' は non-isolated
}
}
エクステンションに付けると、そのエクステンション内のメンバーすべてに nonisolated が波及します。これまでメンバーごとに nonisolated を付けていたのが、1 か所で済むようになります。
nonisolated extension GloballyIsolated {
var x: NonSendable { .init() }
func implicitlyNonisolated() {}
}
struct C: GloballyIsolated {
nonisolated func explicitlyNonisolated() {
let _ = x // OK
implicitlyNonisolated() // OK
}
}
class / struct / enum に付けた場合も、そのメンバーはすべて nonisolated になります。
nonisolated class K: GloballyIsolated {
var x: NonSendable
init(x: NonSendable) {
self.x = x // OK, 'x' は non-isolated
}
}
nonisolated struct S: GloballyIsolated {
var x: NonSendable
init(x: NonSendable) {
self.x = x // OK, 'x' は non-isolated
}
}
ただし、nonisolated 宣言の内部にネストされた型は、自分自身の適合リストから独立して isolation を推論します。外側の nonisolated は内側のネスト型には波及しません。これは @MainActor の推論がネスト型に波及しない従来の挙動と揃っています。
nonisolated struct S: GloballyIsolated {
var value: NotSendable // 'value' は non-isolated
struct Nested: GloballyIsolated {} // 'Nested' は @MainActor-isolated のまま
}
stored property への nonisolated
stored property に nonisolated を書けるルールも広がります。
non-Sendable 型の stored property: 元々 non-Sendable 型の stored property は暗黙に non-isolated として扱われていましたが、今後はこれを明示的に nonisolated と書けます。non-Sendable な基底インスタンス自体が同時に 1 つの isolation domain からしかアクセスされないため、メソッドやプロパティを nonisolated にしても安全です。
class MyClass {
nonisolated var x: NonSendable = NonSendable() // OK
}
Sendable 値型の mutable な Sendable storage: SE-0434 でグローバルアクターに isolated な値型の var プロパティについて導入されたルールを、すべての Sendable 値型へ拡張します。モジュール内では nonisolated が暗黙に推論され、モジュール外からも同期的にアクセスさせたいときは明示的に nonisolated を付けます。
protocol P {
@MainActor var y: Int { get }
}
struct S: P {
var y: Int // モジュール内では 'nonisolated' が推論される
}
struct F {
nonisolated func getS(_ s: S) {
let x = s.y // OK
}
}
Sendable 値型は isolation domain を跨ぐ際にコピーされるので、各 domain は独立した複製を持ちます。プロパティ型も Sendable であれば、var であっても代入が他の domain のコピーに影響することはなく、同期アクセスは安全です。モジュール外へこの緩和を公開したいときは、明示的に nonisolated を書きます。
// Module A
public protocol P {
@MainActor var y: Int { get }
}
public struct S: P {
public nonisolated var y: Int // 'y' は明示的に non-isolated
}
// Module B
import A
struct F {
nonisolated func getS(_ s: S) {
let x = s.y // OK
}
}
明示的な nonisolated を付けていない場合、モジュール外からは従来どおり @MainActor-isolated として扱われます。
nonisolated を書けない場所
次のケースでは nonisolated を書くことができません。
-
他の isolation と同時には書けない。グローバルアクター属性や
isolatedパラメータとnonisolatedを同じ宣言に併記することはできません。@MainActor nonisolated struct Conflict {} // error: isolation attribute が複数ある -
Sendable型のプロパティで、プロパティ型が non-Sendableのとき。Sendableな外側を他の domain に送ったあと、そこから non-Sendableなプロパティへ同期アクセスできてしまうとSendable性と矛盾するためです。@MainActor struct InvalidStruct /* 暗黙に Sendable */ { nonisolated let x: NonSendable // error } -
Sendableクラスのvarプロパティ。参照型は複数の domain が同じインスタンスを共有できるため、varをnonisolatedにすると同期的な並行アクセスが起きてデータ競合になり得ます。@MainActor final class InvalidClass /* 暗黙に Sendable */ { nonisolated var test: Int = 1 // error }
導入時の注意
nonisolated を付けることでグローバルアクター推論が遮断されると、それに連動していた暗黙の Sendable 適合も失われる点に注意が必要です。たとえば class C: GloballyIsolated {} は @MainActor 推論の結果として暗黙に Sendable になりますが、次のように nonisolated を付けると、推論ごと打ち切られて Sendable ではなくなります。
nonisolated class C: GloballyIsolated {}
これまで C の Sendable 性に依存していた呼び出し側のコードは、必要なら明示的に Sendable に適合させるなどの対応が必要になります。