weak let
01 何が問題だったのか
Swiftでは、weak 修飾子を付けた変数やプロパティは必ず var で宣言しなければなりませんでした。参照先のオブジェクトが破棄されると weak 参照は自動的に nil に変わるため、「値が変わる = ミュータブルなストレージでなければならない」という発想からくる制約です。
しかしこの制約は、Sendable(並行安全性)まわりの検査と深刻に噛み合わない問題を引き起こしていました。
Sendable なクラスに weak プロパティを持てない
Sendable に適合するクラスは、ミュータブルな stored property を持てません。ところが weak プロパティは var でなければならないため、この2つを組み合わせようとするとエラーになります。
final class C: Sendable {}
final class VarUser: Sendable {
weak var ref: C?
// error: stored property 'ref' of 'Sendable'-conforming class 'VarUser' is mutable
}
@Sendable クロージャで weak キャプチャが使えない
[weak c] のような明示的キャプチャは、weak が絡むと暗黙にミュータブルとして扱われます。一方、@Sendable なクロージャはミュータブルな変数をキャプチャできません。結果として、明示的に weak キャプチャを書いただけで @Sendable にできなくなります。
func makeClosure() -> @Sendable () -> Void {
let c = C()
return { [weak c] in
c?.foo() // error: reference to captured var 'c' in concurrently-executing code
c = nil // 許されてはいるが、実際に使う場面は非常に稀
}
}
これは利用者から見ると不自然な挙動です。[c] や [unowned c] といった他の明示的キャプチャはすべてイミュータブルなのに、[weak c] だけがミュータブルになる例外になっていました。しかも、weak キャプチャを実際に書き換えるコードはほとんど存在しません。
struct で包むと通ってしまう
より根本的な問題として、「weak は必ず var」というルールは、単一プロパティの struct で weak を包むことで簡単に迂回できました。
struct WeakRef {
weak var ref: C?
}
final class WeakStructUser: Sendable {
let ref: WeakRef // OK
}
func makeClosure() -> @Sendable () -> Void {
let c = C()
return { [c = WeakRef(ref: c)] in
c.ref?.foo() // OK
}
}
この迂回策が成立してしまうこと自体、「weak は var でなければならない」というルールが本質的なものではないことを示唆していました。
また、「オブジェクトが破棄されると値が変わるからミュータブル」という説明は、weak 参照を含む struct の挙動(関数の返り値のようなイミュータブルな値の中にあっても、中の weak は nil に変わりうる)ともうまく整合しません。「変数自体が書き換わる」ことと「weak 参照の指す先を観測すると nil が返るようになる」ことは、本来別の話として扱えるはずでした。
02 どのように解決されるのか
weak を let と組み合わせられるようにし、同時に明示的な weak キャプチャをイミュータブルな扱いに変えることで、Sendable との噛み合わせの悪さを解消します。
weak let が書けるようになる
weak var が書ける場所なら、同じように weak let も書けるようになります。weak var と同じく、型は Optional でなければなりません。
final class C: Sendable {}
final class LetUser: Sendable {
weak let ref: C? // OK(従来はエラー)
}
これにより、weak プロパティを持つクラスでも素直に Sendable に適合できるようになります。「参照先が破棄されて nil を返すようになる」ことは、変数自体の書き換えとは別物として扱われる、という整理です。
明示的な weak キャプチャはイミュータブルになる
[weak c] のような明示的キャプチャは、他の明示的キャプチャと同じくイミュータブルになります。そのため、@Sendable なクロージャ内でも weak キャプチャがそのまま使えるようになります。
func makeClosure() -> @Sendable () -> Void {
let c = C()
// このクロージャは @Sendable にできる
return { [weak c] in
c?.foo()
c = nil // error: cannot assign to value: 'c' is an immutable capture
}
}
本当にミュータブルな weak キャプチャが必要なとき
weak キャプチャへ代入するようなコードを書きたい場合は、weak var を別途宣言してそれをキャプチャします。この場合はミュータブルな変数をキャプチャしているため、クロージャは @Sendable にはできません。
func makeNonSendableClosure() -> () -> Void {
let c = C()
weak var explicitlyMutable: C? = c
return {
explicitlyMutable?.foo()
explicitlyMutable = nil // OK
}
}
段階的導入
weak let を許可する変更はソース互換な追加です。一方、「明示的 weak キャプチャをイミュータブルにする」変更は、わずかとはいえ既存コードを壊す可能性があるため、ImmutableWeakCaptures という upcoming feature flag でゲートされます。フラグを有効化するまでは、従来通り weak キャプチャへの代入が可能です。
仕様の補足
- 関数引数に
weakを付ける構文は存在しません(従来通り)。 - computed property に
weakを書くことは従来通り可能ですが、効果はありません。
背景: weak 参照のスレッドセーフティ
この変更の根拠として、weak 参照のスレッドセーフティのモデルがあります。Swiftの weak 参照は、別スレッドで参照先オブジェクトが破棄されても安全に nil を返すように設計されていて、これは weak var でも weak let でも変わりません。一方で、同じ weak var に対して並行に読み書きすることまでアトミックに守られているわけではなく、そこは他の var と同じく排他制御が必要になります。
つまり「参照先が破棄されて観測結果が nil に変わる」ことと「変数そのものが書き換わる」ことは別レイヤの話であり、後者が起こらない(= let である)ことは前者と何も矛盾しない、というのがこの提案の立脚点です。