非Sendable型のキャプチャと@Sendableクロージャ
問題の説明
Swift 6言語モードでは、セキュア並行性チェックが強化されました。次のコードでTaskクロージャ内でクラスのインスタンスをキャプチャするとコンパイルエラーが発生します:
swift
actor MyActor {
func doWork() -> Int {
return 42
}
}
class MyClass {
let worker: MyActor = MyActor()
func swift6IsJustGreat() {
Task {
// エラー発生:
// Capture of 'self' with non-sendable type 'MyClass' in a `@Sendable` closure
let number: Int = await worker.doWork()
print(number)
}
}
}エラーの原因
MyClassは非Sendable型(クラスはデフォルトで非Sendable)Taskのクロージャは@Sendable属性(並行安全性を要求)- クロージャが
self全体をキャプチャしようとすると問題発生 - Swift 6の新しいセキュア並行性ルールで検知される
重要なポイント
workerはActor(Sendable)ですが、それを保持するMyClassが非Sendableなため、selfを丸ごとキャプチャできません
解決策
方法1: 必要なプロパティのみをキャプチャ(推奨)
キャプチャリストを使用し、self全体ではなく必要なプロパティ(worker)だけをキャプチャします:
swift
class MyClass {
let worker: MyActor = MyActor()
func swift6IsJustGreat() {
Task { [worker] in // workerのみをキャプチャ
let number: Int = await worker.doWork()
print(number) // 正常動作
}
}
}この方法が有効な理由
workerはActor(常にSendable)なので安全にキャプチャ可能selfをキャプチャしないため並行性チェックをパスletプロパティなので実行中に値が変わるリスクなし- 元のコードと同等の機能を維持
プロパティがvarの場合
プロパティがvarの場合でも同様の方法で解決できますが、キャプチャ時にコピーされるため変更時の同期には注意が必要です
方法2: 非同期メソッドにリファクタリング
Taskを使用せずメソッド自体を非同期に変更する方法もあります:
swift
class MyClass {
let worker: MyActor = MyActor()
// asyncメソッドに変更
func swift6IsJustGreat() async {
let number: Int = await worker.doWork()
print(number)
}
}
// 呼び出し側
Task {
await myClassInstance.swift6IsJustGreat()
}この方法のメリット
- 浮動する
Taskを排除(安全な並行処理) - 呼び出し元で明示的に非同期処理を管理可能
- Swift並行性モデルのベストプラクティスに準拠
重要概念の解説
Sendableプロトコル
- 同時アクセスが安全な型を示すマーカープロトコル
- 値型(構造体、列挙型)は自動的に適合
- Actorは常にSendable
- クラスは明示的に適合する必要あり
@Sendableクロージャ
- クロージャが並行コンテキストで安全に使用可能であることを保証
TaskやDetachedTaskのクロージャに自動適用- キャプチャする全ての値がSendableであることを要求
危険なパターン
Task { [weak self] in ... }を使っても解決できません:
swift
Task { [weak self] in
// 依然としてself?.workerへのアクセスが非Sendable
let number = await self?.worker.doWork()
}弱参照でもアクセス経路自体が非Sendableとなると安全性は保証されません
実践ガイドライン
| シナリオ | 対応方法 | 安全性 |
|---|---|---|
| letプロパティへのアクセス | キャプチャリストで直接プロパティをキャプチャ | ⭐⭐⭐ |
| varプロパティへのアクセス | actor内で状態管理するかletに変更推奨 | ⭐⭐ |
| 複数プロパティが必要 | 必要なプロパティを個別にキャプチャ | ⭐⭐⭐ |
| メソッド全体が非同期処理 | メソッド自体をasyncに変更 | ⭐⭐⭐⭐ |
推奨アプローチ
Sendableではない型(特にクラス)ではプロパティ単位でのキャプチャを優先- オブジェクトのライフサイクル管理が必要な場合はActor上で状態を保持
- 可能な限り浮動する
Taskではなくasyncメソッドを使用
swift
// ベストプラクティス例
class DataProcessor {
// 状態管理はActorに委譲
private let processorActor: DataProcessorActor
func processData() {
Task { [processorActor] in
await processorActor.process()
}
}
}
actor DataProcessorActor {
func process() { ... }
}Swift 6の並行性モデルは、安全性を重視することで設計されています。一見制限のように見えても、実際にはランタイムクラッシュやデータ競合を防ぐ保護機能として働いています。キャプチャリストを適切に使用することで、安全かつ効率的な並行コードを実現できます。