Skip to content

非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クロージャ

  • クロージャが並行コンテキストで安全に使用可能であることを保証
  • TaskDetachedTaskのクロージャに自動適用
  • キャプチャする全ての値が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に変更⭐⭐⭐⭐

推奨アプローチ

  1. Sendableではない型(特にクラス)ではプロパティ単位でのキャプチャを優先
  2. オブジェクトのライフサイクル管理が必要な場合はActor上で状態を保持
  3. 可能な限り浮動するTaskではなくasyncメソッドを使用
swift
// ベストプラクティス例
class DataProcessor {
    // 状態管理はActorに委譲
    private let processorActor: DataProcessorActor
    
    func processData() {
        Task { [processorActor] in
            await processorActor.process()
        }
    }
}

actor DataProcessorActor {
    func process() { ... }
}

Swift 6の並行性モデルは、安全性を重視することで設計されています。一見制限のように見えても、実際にはランタイムクラッシュやデータ競合を防ぐ保護機能として働いています。キャプチャリストを適切に使用することで、安全かつ効率的な並行コードを実現できます。