Sendableは、スレッド間での受け渡しに安全な型をマークするSwiftのプロトコルです。並行処理(actor、async/await、Task)を扱う場合、Swiftコンパイラは、隔離されたコンテキスト間で受け渡しされるすべてのデータがSendableに準拠することを要求します。これにより、データ競合を引き起こす可能性のある安全でない型が誤って受け渡しされるのを防ぎます。このプロトコルは契約として機能します。Sendableに準拠した型は、内部に同期されていない状態がないことを保証します。WWDC 2021によると、Sendableプロトコルは安全なマルチスレッドアーキテクチャを設計する上で必須の要素です。
重要ポイント
Sendableは、Swift標準ライブラリ(SE-0302)のプロトコルで、隔離ドメイン間での受け渡しに安全な型をマークします。隔離ドメインとは、actor、Task、@MainActorコンテキストを意味します。Sendableに要件はありません — セーフティマーカーとして機能します。
Swift 5.5以前は、開発者はDispatchQueueを介して任意のオブジェクトをスレッド間で受け渡しでき、コンパイラはそれが安全かどうかをチェックしませんでした。Sendableはこのギャップを埋めます。今ではコンパイラ自体がスレッド間の転送を追跡し、安全でないものをブロックします。これにより、並行コードは実行前からより信頼性が高くなります。
struct UserProfile: Sendable {
let name: String
let age: Int
}
class NonSendableClass {
var counter: Int = 0
}
この例では、UserProfileはSendable型の定数プロパティを持つ構造体であるため、スレッド間で安全に受け渡しできます。NonSendableClassは、awaitを介してまたはTaskに受け渡そうとするとコンパイルエラーになります。
SwiftのSendable型は3つのカテゴリに分類されます。Sendableプロパティを持つ値型、不変状態のfinalクラス、@Sendableでマークされた関数/クロージャです。コンパイラは値型に対して自動的にSendable準拠を推論します。
Struct、Enumおよびタプルは、すべてのプロパティと関連値がSendableであれば自動的にSendableになります。これは保守的なアプローチです。1つのプロパティでもSendableに準拠していない場合、型全体が安全として認識されません。
| 型 | Sendable | 条件 |
|---|---|---|
| Struct | はい(明示的な宣言なし) | すべてのプロパティがSendable |
| Enum | はい(明示的な宣言なし) | すべての関連値がSendable |
| Final class | はい(明示的な宣言あり) | すべてのletプロパティがSendable、varなし |
| Non-final class | いいえ | 継承のためSendable不可 |
すべてのSwift組み込み型 — Int、String、Double、Bool、Optional、Array、Dictionary、Set — はSendableに準拠しています。これにより、型の合成がデフォルトで安全になります。開発者はカスタムクラスにのみ注意する必要があります。
@unchecked Sendableは、クラスがコンパイラのチェックをバイパスして明示的に自身をSendableと宣言できるメカニズムです。開発者はそのようなクラスのスレッドセーフティの責任を負います。これはObjective-Cブリッジや最適化された構造に役立ちます。
@unchecked Sendableは、クラスが内部的にロックやアトミック操作によって安全性を保証しているが、コンパイラが静的に検証できない場合に使用されます。たとえば、os_unfair_lockやpthread_mutex_tを持つクラス — そのスレッドセーフティはコードによって保証されていますが、Swiftはそれを認識しません。
final class AtomicCounter: @unchecked Sendable {
private var value: Int = 0
private let lock = NSLock()
func increment() {
lock.lock()
value += 1
lock.unlock()
}
}
@unchecked Sendableの使用は慎重に行う必要があります。これは他の開発者への明確なシグナルです。「確認済み、安全です」。実装のミスは発見が困難な競合を引き起こす可能性があります。@uncheckedを使用する前に、型が本当に値型として書き直せないことを確認してください。
ActorとSendableは表裏一体です。Actorはその状態を隔離しますが、外部の世界とデータを交換するにはSendable型を返す必要があります。Actorメソッドが非Sendable型を返す場合、コンパイラは警告またはエラーを発します。
Actorが外部コードにデータを送信するとき、このデータは隔離境界を越えます。Sendableは、受信者がActor外でデータを安全に使用できることを保証します。Actor自体は隔離されたままです — その内部状態は公開されません。
struct AccountSnapshot: Sendable {
let id: UUID
let balance: Double
let lastUpdated: Date
}
actor BankActor {
private var balance: Double = 0
func snapshot() async -> AccountSnapshot {
return AccountSnapshot(
id: UUID(),
balance: balance,
lastUpdated: Date()
)
}
}
AccountSnapshotは、Sendable型のletプロパティのみを持つSendable構造体です。このアプローチはActorからデータを抽出するためのベストプラクティスです。状態のスナップショットは値で渡され、Actorはその状態の制御を失いません。
@Sendableは、クロージャが非Sendableデータを可変形式でキャプチャしないことを保証する関数とクロージャの属性です。クロージャがTaskやActorメソッドに渡されるとき、それはSendableでなければなりません。
コンパイラは、@Sendableクロージャがクラスへの可変参照をキャプチャしないことをチェックします。Sendable型のletプロパティのキャプチャは許可されています。参照型のvar変数をキャプチャすると、クロージャがミューテーションと同時に実行される可能性があるため、エラーになります。
func performAsync(operation: @Sendable () -> Void) {
Task {
await operation()
}
}
let constant = "Safe"
var counter = 0
// counter — varをキャプチャするとcounterがミューテートされ、コンパイルエラー
performAsync { // ❌ キャプチャされた変数のミューテーション
print(constant)
}
ルールはシンプルです。@Sendableクロージャは、それ自体がSendableであり、外部からミューテーションされないデータのみをキャプチャできます。クラスの場合、クラスがSendableとしてマークされていなければ、弱参照weak selfのキャプチャが許可されます。これにより、古典的な retain cycle や競合状態を防ぎます。
よくある質問
Sendableは、コンパイラに「この型はスレッド間で安全に受け渡しできます」と伝えるマーカーです。構造体や定数は通常デフォルトでSendableですが、クラスは明示的に指定しない限りそうではありません。
Sendableプロパティを持つ値型(struct、enum)、不変状態のfinalクラス、およびすべてのSwift基本型(Int、String、Double、Bool、Array、Dictionary、Optional)は自動的にSendableに準拠します。
@unchecked Sendableは、コンパイラの静的チェックなしでクラスをSendableとして宣言する方法です。開発者はロックやアトミック操作などを通じて、自身で安全性を保証します。
Actorはその状態を隔離しますが、外部コードにデータを返すとき、このデータは隔離境界を越えます。Sendableは、受信者がActor外でこのデータを使用する際に競合に遭遇しないことを保証します。
すべてのプロパティがSendable型の定数であるfinalクラスにSendableプロトコルを追加します。クラスがロックを使用する場合は@unchecked Sendableを使用できますが、注意が必要です。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。