Swift 6 への移行や、古いコードとの併用では「並行性チェック」をどう緩和・保証するかが大きな課題ですよね。そこで登場するのが @preconcurrency
と MainActor.assumeIsolated
。一見、どちらも「コンパイラやランタイムのチェックを回避する仕組み」に見えますが、目的や動作タイミングがまったく異なります。本記事では、それぞれの使い分けを解説します。
1. シチュエーション:古いコードとメインアクター
たとえば、あなたがゲームを作っているとします。
- ゲームエンジン (GameEngine): 昔からあるライブラリで作られている
- 新しい Swift Concurrency: メインアクター(
@MainActor
)を使って画面更新を安全にしたい
このとき、古いゲームエンジンは「どのスレッド(アクター)で呼ばれるか分からない」状態のデリゲートを持っているかもしれません。すると、Swift 6 などの厳しい並行性チェックでエラーや警告が出ることがあります。
2. @preconcurrency
:古いコードのコンパイルエラーを緩和する
2.1 どんなときに使う?
- 古いライブラリをインポートしたいけど、Swift 6 だとコンパイルエラーがいっぱい出て困るとき
- ライブラリが「メインスレッドで呼ぶかどうか、昔の書き方であいまい」
ここで @preconcurrency
を使うと、コンパイラがエラーを警告にしてくれる など、ビルドが通りやすくなります。
2.2 具体例
@preconcurrency import OldGameEngine
こう書くと、「このライブラリはコンカレンシーの新しいルールを完全には適用しないでね」とコンパイラに伝えることになります。
2.3 でも安全なの?
- 実行時にチェックはありません
- メインアクターじゃないところでUIをいじっても、
@preconcurrency
は止めてくれません - 「コンパイルエラーを減らすだけ」で、根本的に安全になるわけではない
3. MainActor.assumeIsolated
:実行時にメインアクター以外ならクラッシュ
3.1 どんなときに使う?
- 本当はメインアクターで呼ばれていると確信しているけど、コンパイラには証明できない場合
- メインアクター以外から呼ばれると困るので、間違いならクラッシュさせたい
3.2 具体例
nonisolated func oldEngineCallback() {
MainActor.assumeIsolated {
// 実際にメインアクターじゃなかったら、ここでクラッシュ
updateUI()
}
}
nonisolated メソッド内で MainActor.assumeIsolated
を呼ぶと、実行時に「メインアクターかどうか」をチェックし、違えば止まります。
3.3 クラッシュすることでユーザーデータ破損を防ぐ
- Swift Concurrency は「競合して壊すより、クラッシュで気づかせる」考え方
- 実際にメインアクター以外で呼ばれたら即止まるので、データ破損を避けられる
- ただし本当にメインアクター以外で呼ばれないと確信できるときだけ使う
4. 使い分けまとめ
@preconcurrency
- コンパイル時 の並行性チェックを緩和
- 古いコードやライブラリでエラーを減らしたいとき
- 実行時にメインアクターかどうかは確認しない
MainActor.assumeIsolated
- 実行時 にメインアクターかどうかを強制チェック
- 違えば即クラッシュしてデータ破損を防ぐ
- 最終手段的な方法
5. どちらも正しい並行性に移行したほうがいい
-
@preconcurrency
は「とりあえず古いコードをビルド通す」だけで、実際の安全性を保証するわけではありません -
MainActor.assumeIsolated
は「ランタイムで強制的にチェック」する最終手段 - いずれも「本来は
@MainActor
、async
、Sendable
などできちんと静的に証明できる形」が理想
まとめ
-
@preconcurrency
はコンパイラに「古い仕組みだから厳密チェックしないで」と頼むための属性 -
MainActor.assumeIsolated
は実行時に「ここはメインアクター」と強制し、違えば即クラッシュ - どちらも暫定措置としては便利だが、最終的には正しい並行性モデルにリファクタリングするのが望ましい