Swift 6 に関するあれやこれや

何も考えず言語モードを 5 から 6 にしてビルドしたらエラー欄が真っ赤になってひえってなった人こんにちは。
ZYYX の幻獣枠その１、後藤です。（その２の人もなんか記事書きましょう）
社内ではモバイルアプリの開発メインで活動しています。
関数型言語、強い静的型付け言語が大好きなので、モバイル系で仕事にすんなり使えるものの中では Swift が一番のお気に入り。

社内勉強会向けに書いていたものを、どうせだから記事にしろと言われたのでそのまま貼ります。
本当は実際に Swift 6 対応した際の差分なんかを見せる予定もあったんですが、実案件のプロジェクトをベースにしているため外には出せません。悪しからず。

多少なりと Swift に触れたことがある（できれば Swift Concurrency にも）人をターゲットとしています。

# Swift 6 対応への心構え

## `isolation` を意識する

### `isolation` とは？

* 直訳すれば『分離』や『隔離』などで、つまりは隔離された実行空間である
* 実際にどういうものかというと概念としてはスレッドみたいなものと思えば良いはず
* 平行して走るプログラムの流れであり
* 互いにデータをやり取りする際には気をつけないと変なことが起きるよというもの
* 現状の Swift において `isolation` はほぼ `actor` とイコールであると思って良い
  * `DispatchQueue` なんかも含まれるはずだけど混ぜると危険なのでひとまず置いておく

### `actor` とは？

* それぞれが個別の `isolation` （隔離された実行空間）を持っていて内部状態は常にその中でしか変更ができない
* つまり別の `isolation` から直接中身に影響を及ぼすことはできない

### `globalActor` とは？

* みんなが使える共有された `actor`
* デフォルトで存在するのは `MainActor` のみ
  * `MainActor` が持つ `isolation` はメインスレッドである
  * さまざまなしがらみにより `DispatchQueue.main` と実質的に同じものである（はず）
* 定義すると `@MainActor` のような属性として使えるようになる
* この属性をつけた `class` はその `globalActor` を `isolation` として持つ `actor` のように振る舞う

### Swift 6 における `isolation`

* `class` や `struct` はデフォルトでは `nonisolated` （ `isolation` 不明）である
* `actor` はそれぞれが個別の `isolation` である
* `globalActor` の属性で修飾された要素はその `globalActor` が `isolation` である
  * `globalActor` の指定は `class` などの型単位だけでなく `extension` や `property`, `func` ごとに指定することもできる
* `nonisolated` ではない状態（いずれかの `isolation` に属している）を `isolated(any)` と表現する場合あり

そして、ある `isolation` (`nonisolated` 含む) から別の `isolation` にアクセスするにはいろいろな制限がある。

## 何はなくとも `Sendable`

### `Sendable` とは？

* 訳すと『送付可能』と言ったところか
* 要するに `isolation` 間で送ることができる値であることを示すマーカーである
* 逆に言うと、 `Sendable` ではない値は `isolation` 間で（直接には）やりとりできない

### `Sendable` ではない値がやりとりできないようになっている理由

* Swift 6 で導入された Strict Concurrency Checking が目指すものは Data Race が存在しないようにすること
* Data Race （データ競合）とは制御されていない平行処理によってデータの状態が予期できない不安定な状態になること
  * スレッドプログラミングで取り沙汰される Race Condition と概ね同じものと思って良い
  * Race Condition が発生しているとデータの状態が不安定になり予測できない（再現性の低い）厄介なバグの元になる
  * 最悪クラッシュやセキュリティホールの原因になり得る
* Data Race が起こらないデータを `Sendable` とマーキングすることで、データが不安定にならないことを保証する
* 『データが不安定』って？
  * 同じデータを全く同じ流れで処理しているのになぜか結果が違うと言う状況
  * 一つのオブジェクトを複数のスレッドで変更している場合などに起きやすい
    * ある配列の中身を書き換えていってる時に、同時に別のスレッドからも似たようなことをすると結果がぐちゃぐちゃになったり
* Data Race を起こさないためには常にどこか一つの isolation でしか変更がされないことを保証する
  * いくつかアプローチはあるがスレッドプログラミングでは一つのスレッドからしかアクセスできないような排他制御機能を使う
  * 一つのオブジェクトが複数の `isolation` で平行して変更がされたとしても、それが全く同時に行われることは無くなる
  * そうなっていれば、仮に一見不可解な動きが見えたとしても、実行される順番を考慮すれば動作を予測することができる

### `Sendable` の要件

`Sendable` であるための概念的な要件は次のようになる。

* 値の変更が不可能である
* 値の変更が可能であるが、何らかの方法（排他制御）で Data Race が起きないようになっている

より具体的な要件を示すと以下のようになる。

* `actor` である
  * `actor` はそれぞれプライベートな `isolation` を持っていてその中でしか内部状態を変更できない
  * `actor` は自動で `Actor` プロトコルを実装していて、`Actor` は `Sendable` を含むので明示的に `Sendable` であることを宣言する必要はない
* `Sendable` な値のみで構成された `struct` である
  * `struct` は値型（概念的にはコピーによって渡されるもの）であるため本質的に変更不可である
  * `var` プロパティがあったとして、それを使った場合に元の値が変更されるわけではない（新しいコピーが作られる）
  * 自動で `Sendable` のチェックが行われるので明示的に宣言する必要は通常ない
* 全ての `case` が `Sendable` な値のみで構成された `enum` である
  * `enum` は不変値なので中身が全て `Sendable` であれば `Sendable` になれる
  * 自動で `Sendable` のチェックが行われるので明示的に宣言する必要は通常ない
* 次の要件全てを満たす `class` である
  * `final` である
    * 継承可能になっていると継承先が非 `Sendable` であった場合でも言語仕様上区別がつかなくなるため
    * `Sendable` なクラスを継承したら子クラスも必ず `Sendable` にしなければいけないルールにすれば良いのではと思えなくもないが…
      * 親クラスの中身がわかっていないと `Sendable` を保証できない可能性がある
      * それに親クラスを **後から** `Sendable` にしたりした時に子クラスが全部壊れるとか悪夢でしかない
  * 親クラスを持たない、もしくは、 `NSObject` の直接のサブクラスである
    * 親クラスが `Sendable` であることを保証できないため
    * `Sendable` なクラスを継承すれば良いのではとも思えるが `final` であることが要件であるためそもそも継承できるようなクラスが存在し得ない
  * 全ての Stored Property が `Sendable` である
  * 変更可能な Stored Property を持たない
* これまでのいずれも満たさない場合でも、型全体が `globalActor` で修飾されている
  * 個別のプロパティやメソッドではなく、型自体が `globalActor` 修飾されると、ほぼ `actor` と同等の扱いになる
  * `globalActor` で修飾されたクラスは継承先も同じ `isolation` に属するようになる
    * つまり、例えば `UIViewController` を継承したクラスなどは全て `MainActor` が `isolation` である

## Swift 5.x -> 6.0

これまで述べたとおり、Swift 6 では `isolation` と `Sendable` を強く意識する必要がある。
そして、それらが正しく扱われていない場合、警告どころかエラーになりコンパイル自体できない。

言語モードは 5 のまま Upcoming Features で 6 相当の設定をオンにして対応していくのが無難（エラーの代わりに警告になる）

ref. [Swift 6 Migration Strategy](https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/migrationstrategy)

既存のコードを初めて 6 に移行しようとした際に発生するエラーの典型的なパターンを挙げてみる。

### `static` な Stored Property はほぼ全滅

* `static` プロパティは最初にアクセスされた時に初期化されるルールになっているため、そのままでは初期化処理が行われる `isolation` が特定できない
  * 逆に言えば `isolation` が特定できれば良いので、対応の方針としては次のようになるだろう
  * `class` を `actor` に変える
    * `actor` が `isolation` なのはインスタンス化された状態の話なので `static` は `isolation` が特定できない気がする
    * のだが、おそらく何か特別な扱いをされているっぽい
    * どんな文脈からも `await` も無しでアクセスが可能
    * 既存クラスを変更してしまうと影響箇所が大きくなりすぎる可能性がある
      * プロパティだけ新規の `actor` に切り出すような形の方が修正量は少なくなる
  * 型自体、もしくはプロパティに globalActor を指定する
    * プロパティの isolation が globalActor によって特定されるので、初期化処理が行われる isolation も決まる
    * 古いプロジェクトで `await` が使えないコンテキストが大多数、かつ、複数の `isolation` からアクセスする必要があるような場合はちょっと使いにくい
  * `actor` にすることはできないが `nonisolated` な状態（どの `isolation` からでもアクセス可能）にしたい
    * 保持する値の型は `Sendable` であること
    * `immutable` （ `let` での定義）であること
* これから新たに定義するなら適当に `actor` を作ってそこに持たせるのが楽そう
  * ただまあ、そもそもあんまりそういった広範囲で共用するようなものは Swift 6 とか関係なしになるべく避けようよ
    * testable じゃないとか色々

### `lazy var` もだいたい全滅

* 実質的には `immutable` でも `self` やその他、後にならないと使えるようにならない要素を使って初期化する必要があるような場合によく使っていた
  * 他にも、最初に1回だけ処理をさせたいようなパターンなんかにも使われたりする
* ダメな理由は `static` プロパティと同様で、そのままだと初期化処理の `isolation` が特定できないため
  * よって、 `actor` や `globalActor` で `isolation` を固定してやれば良い
  * `var` （ `mutable` ）であるため `static` プロパティのように `nonisolated` な状態にすることはできない
    * 自己責任で行う方法はあるがひとまずここでは説明しない

### 自作の `Error` 準拠型

* `protocol Error` は `Sendable` を含んでいるため、構成要素まで全てが `Sendable` でなくてはいけない
  * 原因となったオブジェクトをそのまま持たせたりする場合があるが、それが非 `Sendable` なためにエラー
  * とりあえず後でどうにかするという想定で雑に `Any` 型で保持していたりすると（`Any` は `Sendable` にはならないので）エラー
* 一応 `actor` 化したり `globalActor` をつけることで回避できるが扱いがめんどくさくなりそうなので疑問
  * `nonisolated` のまま `Sendable` に準拠するように直す方がおそらく良い気がする

### `awakeFromNib` でエラー

* `awakeFromNib` は `NSObject` のメソッドで `nonisolated` である
* なのに通常これが使われる（オーバーライドされる）のは `@MainActor` がついたクラスを継承している場合である
* 当然ほぼ全ての要素は `isolated(MainActor)` の状態なので、 `nonisolted` なコンテキストから直接 `isolated` な要素に触ろうとしてエラーになる
* 特殊なことをやっていない限り `UIKit` の要素の `awakeFromNib` がメインスレッド外で呼ばれることはないはず
  * であれば `MainActor.assumeIsolated` を使っておけば大丈夫
  * これは現在の `isolation` が `MainActor` であると仮定して処理を強制的に行うためのメソッド
    * メインスレッド外で実行された場合にはここでクラッシュする

### 既存プロジェクトを Swift 6 対応した結果得た感触について

大きな一つの指針としては次のようになると思われる。

***あらゆる要素を可能な限り `Sendable` にする***

* `Task` 内（ `async/await` が使えるコンテキスト）中に `Sendable` ではない要素が出てくるといちいち詰まる
* `@escaping` なブロック（クロージャ）で `Sendable` ではないものをキャプチャしようとするとエラーになる

などなど、 `Sendable` ではない要素は Swift 6 の世界においては非常に扱いにくいものになっている。
逆に言うと、あらゆるものが `Sendable` であればほとんどの場面で面倒なことを考えずともコードが書けるし、かつ、Data Race の可能性を極小化した状態を Swift 言語自体が保証してくれる。

#### `Sendable` にするには、すでに書いた要件を満たしていけば良い

つまりは以下のとおり。

* `isolated` で良いなら
  * `actor` にする
  * `globalActor` を指定した `class` にする
* `nonisolated` にする必要があるなら
  * `struct` や `enum` にする
  * `final` にするなどの全ての条件を満たした `class` にする

それを踏まえつつ、既存プロジェクトを（手間をなるべく小さく） Swift 6 対応するためのアプローチとしては次のようになると思われる。

* `Sendable` にするために `class` を `actor` に置き換えるのは使う側への影響が大きくなるので避ける
  * 使用する側のコードを `Task` 化しないと何も書けない
  * すでに `Task` 化されていても使用箇所全てに `await` を追加していかないといけない
  * （新規で機能を追加するような場合はバンバン `actor` 使うと良い）
* `class` をマジメに（`nonisolated` な） `Sendable` にするのは最低限に
  * 元々 `mutable` な Stored Property を持たない不変な `class` で何も継承していないなどのラッキーなケースであればアリ
  * `class` にしてる時点であまりそう言うことは無さそうだけど
  * （実装の共通化は継承ではなくミックスインでやる時代なので新規に設計する時はそのようにしておけば `Sendable` にしやすいと思う）
* `class` には基本的に `globalActor` を指定する
  * 同じ `globalActor` に属しているものは `Task` とか `await` とか無しでアクセスできるので `@MainActor` なものとの連携が多いもの（ViewModelとか）は `@MainActor` にしておくとコードの修正量が減る
  * メインスレッドで実行されると困るもの（重たい処理とか）は自前の `globalActor` 定義を追加してそちらを使う
    * これによって `@MainActor` な要素とは `isolation` 境界ができてしまい `Task` や `await` の追加が必要になるが、それは仕方ないのでがんばって直す

#### それでもどうしても `Sendable` にできない要素は残るが、それはどうすべきか

* 外部のライブラリで提供されていて Swift 6 に対応していないもの
* システムライブラリで提供されているが `Sendable` ではないもの

こういったものについては以下のようなアプローチをした。

* ソースコードやマニュアルから十分スレッドセーフのための手当てがされていると判断できる場合
  * 空の `extension` を作成し `@unchecked Sendable` を付加
    * その旨のコメントも添えて
    * ***`@unchecked` をつけるとコンパイラによる `Sendable` 安全性のチェックが行われなくなるので、説明無しに安易につけてはいけない***
* スレッドセーフではない、あるいは、判断がつかない場合
  * 自前で排他制御を行う
  * プライベートなプロパティに値を保持し、常に排他制御内でしかアクセスできないように仕掛けを入れる

#### 非 `Sendable` な要素についてはこれだけでは対処できない場合もある。例えば `Realm` など

* `Realm` クラスのインスタンスと、それから生成される各種オブジェクト類は `Realm` が生成されたスレッド以外から触ることができない
* `Realm` のインスタンス化からそれ以降の全ての操作が同じスレッド（`isolation`）で行われるように制御する必要がある
* 既存コードとの兼ね合いなどから、専用の `globalActor` を用意して `Realm` に関する操作が常にそこの中で行われるようにした
  * 新規に設計するなら `actor` に閉じ込めるようにすると楽だろう
  * 最近のバージョンでは特定の `actor` と連動する形式の API が追加されているのでそれらを使うと良い

#### `isolation` の問題としては `RxSwift` が結構悩ましい問題を孕んでいて困る

* `RxSwift` では登録した `Observer` 等が実行されるスレッド（`isolation`）を `observe(on:)` や `subscribe(on:)` などで指定することができるが、これが Swift 6 の isolation check と非常に相性が悪い
  * Swift 6 のルールではクロージャは作成した場所の `isolation` を引き継ぐ
  * つまり `isolated(MainActor)` な場所で `subscribe` で登録したクロージャはメインスレッドで実行されることが期待されるのだけど、Rx 側の動きによっては別のスレッドで実行される可能性が多分にある
    * そうなった場合はクラッシュ
* `RxSwift` 側が十分に Swift 6 に配慮したものを出してくれるのが理想だけど…
  * そうなるまでの間はどうするか
  * `subscribe` に関しては必ず `observe(on: MainScheduler.instance)` とかしてメインスレッドで実行されるようにしつつ、渡すブロックを `@MainActor` で修飾しておくことで安定はしそう
    * `@MainActor` のコンテキストで `subscribe` する場合は『ブロックを `@MainActor` で修飾』は不要（勝手にそうなる）
    * `subscribe` は値を返す必要もないので重たい処理をメインスレッド外に逃したい場合は単に `Task` を生成すれば良い
  * `map` とか `filter` に関しても先に `observe(on: MainScheduler.instance)` しておくと安心かもしれないが、クロージャを `@Sendable` で修飾しておけばそれで良いかも
    * [こちらの提案](https://github.com/ReactiveX/RxSwift/issues/2638) が取り込まれれば気にしなくて良くなりそう？
* 新規プロジェクトではそもそも `RxSwift` を使わない選択もありだと思う
  * `AsyncStream` などで `Rx` のような機能はほとんど賄えるはず
* とはいえ `RxSwift` にどっぷり浸かっている既存プロジェクトで `Rx` を排除するのは容易ではない
  * `RxSwift` が常に `isolated(any)` かつ `async` なクロージャ（ブロック）を受け取るような API 構成になってくれれば使う側は余計なこと考えなくて良くなりそうな気がするが
    * そんな定義がうまいことできるのかはちょっとよくわからない

## まとめっぽいもの

* Swift 6 に対応していこう
  * Swift 6 のルールに従うことで Data Race を防ぐために今まで自前でやっていた色々なことが自然に実現できるようになる
    * まじめにやろうとすればするほどひたすらめんどくさくなるので、これまではある程度妥協していた部分もあるはず
    * ルールに従うのは、一時的には縛りがキツくなって動きにくいと感じられるかもしれないけど慣れるよ、というか慣れた方が良いよ
  * メインスレッドでしか触っちゃいけないもの（特にUI系）をうっかりダメな場所で触ろうとしちゃってもコンパイラが教えてくれる
    * 初心者に対して口を酸っぱくして説明しなくても良い
* Swift に限らず他の言語でも
  * `isolation` を意識しよう
    * 現代の GUI アプリケーションにおいて平行プログラミングは切っても切り離せない要素
    * 名前はともかく `isolation` に相当する概念はどの言語にもある（ `thread` , `actor` , `coroutine` , `future`, etc）
    * それらを常に意識し、Swift 6 で怒られるようなことは他の言語でも（たとえエラーにはならなくとも）行わないようにすることでより良いコードが書ける可能性がある
  * `Sendable` を意識しよう
    * `Sendable` の概念的要件（不変である、もしくは、排他制御に守られている）を満たすを意識することで Data Race の無い、結果が安定したコードを書ける
    * 特に『不変』な値を多用し、『可変』な箇所は最小限に切り出すことで testability も上がる