🔒

なぜSwiftのSendableはデフォルトにならなかったのか

に公開

目次


1. はじめに

本記事は、筆者がSwift EvolutionやSwift Forumsの議論を調べてみて考えたことをまとめたものです。公式見解ではないですし、すべてを調べ尽くしたわけでもありません。「こういう見方もあるんだな」くらいの気持ちで読んでいただければ嬉しいです。

最近のSwiftの言語仕様を学んでいると、SendableCopyableという2つのマーカープロトコルに出会います。

この2つ、似たような役割を持っているのに、デフォルトの扱いが真逆になっています。

Protocol デフォルト動作
Copyable 全ての型が暗黙的に準拠(opt-out: ~Copyable
Sendable 明示的な準拠が必要(一部推論あり)

Copyableはデフォルトで全ての型が準拠していて、コピー不可にしたい場合は~Copyableで明示的にopt-outします。一方、Sendableは基本的に明示的に宣言する必要があります。

なぜこのような違いがあるのでしょうか?


2. Copyableがデフォルトな理由

まず、CopyableSendableがそれぞれ何を表すプロトコルなのかを整理しておきます。

Protocol 関心事 制御するもの
Copyable 所有権(Ownership) 値を複製できるかどうか
Sendable 並行処理の安全性 Isolation Boundaryを越えて値を渡せるかどうか

どちらも「値へのアクセスを安全に管理する」という点では共通していますが、関心事が異なります。

Copyableは所有権(Ownership)に関するプロトコルです。Swiftでは通常、値は自由にコピーできますが、ファイルハンドルやデータベース接続のような「唯一の所有権」を持つべきリソースには、コピーを禁止したい場合があります。~Copyableを使うと、そのような「move-only」な型を定義できます。

Sendableは並行処理に関するプロトコルです。異なるActor Isolation Domain間で値を受け渡しする際に、データレースが発生しないことをコンパイル時に保証します。

Copyableがデフォルトになっている理由はシンプルです。

SE-0390より:

"All currently existing types in Swift are copyable"

Swift 1.0のころから、全ての型はコピー可能でした。これは言語の暗黙の前提として最初から存在しており、Swift 5.9のSE-0390でCopyableプロトコルとして明示化されました。

つまり、もともと全ての型がコピー可能だったので、後からNoncopyableという概念を導入するときに「デフォルトはCopyable、例外的に~Copyableでopt-out」という設計が自然だったわけです。


3. Sendableがデフォルトになりそうな理由

ここで、Sendableもデフォルトになるべきなんじゃない?という疑問が浮かびます。実際、そう思う理由はいくつかあります。

Swift Concurrencyでは、Actor Isolation Domain(アクター分離ドメイン)という概念があります。異なるIsolation Domainの境界(Isolation Boundary)を越えて値を渡すには、その型がSendableに準拠している必要があります。

典型的なiOSアプリだと、こんな設計パターンが多いと思います。

  • UI層: @MainActorを付けて、MainActor(グローバルアクター)に隔離する
  • UseCase/Repository層など: actor型にしてインスタンスレベルでアクター隔離する。または、イミュータブルなクラスにしてSendableを付ける

Actorは自動的にSendableに準拠しますし、イミュータブルなクラスもSendableとして定義できます。

こう考えると、Sendableである状態がSwiftの型システムにおける標準的・理想的な状態と言えそうです。Isolation Boundaryを越えられる型が基本で、越えられない型(non-Sendable)は例外的な存在のように思えます。

Copyableがデフォルトで、~Copyableでopt-outする設計なら、Sendableも同じように「デフォルトでSendable、~Sendableでopt-out」という設計があり得たはずです。

実際、Swift Forumsでもこういう意見が出ています。

Some thoughts on the proliferation of Sendableより:

"I wonder, if it were added now, would that in fact be expressed more strongly by making it opt-out instead of opt-in, i.e. ~Sendable?"


4. Sendableがデフォルトにならなかった理由

ではなぜSendableはデフォルトにならなかったのか。

一番大きいのは後方互換性の問題です。

Copyableの場合、「全ての型がコピー可能」というのは言語が生まれたときからの事実でした。しかしSendableは異なります。「全ての型がデータレースセーフ」というのは、歴史的に保証されていませんでした。

もしSendableをデフォルトにした場合、既存の膨大なコードに対して破壊的変更になります。データレースセーフではない型が突然Sendableとして扱われ、コンパイルエラーが大量発生します。

要するに、Copyableは「もともと全ての型がコピー可能だった」から自然にデフォルトにできたけど、Sendableは「もともと全ての型がデータレースセーフだったわけじゃない」から、同じようにはできなかったわけです。


5. なぜpublic型には暗黙的なSendable準拠がないのか

「デフォルトにならなかった理由」とは別に、もう一つ整理しておきたい話があります。暗黙的なSendable準拠の範囲についてです。

Swiftでは、ローカルやinternal型については、全てのプロパティがSendableであれば暗黙的にSendable準拠が推論されます。しかし、public型やopen型には暗黙的な準拠が適用されません。

SE-0302より:

"Public non-frozen structs and enums do not get an implicit conformance, because doing so would present a problem for API resilience: the implicit conformance to Sendable would become part of the contract with clients of the API, even if it was not intended to be."

これは「APIコントラクト」の問題です。具体例で説明します。

// ライブラリ v1.0
public struct UserData {
    public let id: Int
    public let name: String
}

もし暗黙的なSendable推論がpublic型にも適用されると、このUserDataは全プロパティがSendableなので、自動的にSendableに準拠します。ライブラリの利用者は以下のように書けるようになります。

// 利用者のコード
let userData = UserData(id: 1, name: "Alice")
Task {
    await processOnAnotherActor(userData)  // Sendableなので渡せる
}

ここで、ライブラリ作者がv2.0で内部実装を変更したとします。

// ライブラリ v2.0
public struct UserData {
    public let id: Int
    public let name: String
    internal var cache: NSCache<NSString, AnyObject>  // non-Sendableを追加
}

この瞬間、UserDataSendableではなくなり、利用者のコードがコンパイルエラーになります。ライブラリ作者は意図せずSendable準拠という公開契約を作ってしまっていたわけです。

public型には明示的なSendable宣言を要求することで、「この型がSendableであることは意図的なAPI設計である」と表明させる設計になっています。

とはいえ、Swift Forumsでは「public型にも暗黙推論すべき」という意見も出ています。

Some thoughts on the proliferation of Sendableより:

"Bottom line up front: the default inference behavior for Sendable is wrong; it should be inferred for public types too."

"I've been transitioning to the experimental strict concurrency checking mode, and I am taken aback by the sheer volume of types that need Sendable annotations."


6. Swiftの方向性

ここからは僕の解釈ですが、Swiftは「Sendableをデフォルトにする」方向ではなく、Sendableの必要性自体を薄くしていく方向に進んでいるように見えます。

SE-0414で導入されたRegion Based Isolationにより、コンパイラがデータフローを解析して、non-Sendableな値でも安全に転送できるケースを検出できるようになりました。これは言い換えると、従来は「Sendableに準拠していないとIsolation Boundaryを越えられない」という厳格なルールだったのが、「転送後に元の場所で使われないなら、Sendableでなくても安全に転送できる」というように、コンパイラのチェックが賢くなったということです。結果として、Sendableを付ける必要があるケースが減りました。

SE-0430ではsendingキーワードが導入されました。Task.initのクロージャなどで、キャプチャする値がSendableでなくても、安全であることが証明できれば許容されるようになっています。ちなみにsendingは、Copyableにおけるconsumingと似たような対応関係にあります。どちらも「移転したら元の場所では使えなくなる」という点で共通しています。

Swift 6.2ではモジュール単位で@MainActorをデフォルトにする仕組みが導入されました。これにより、アプリ開発者は「基本はMainActorで動いて、必要なところだけ明示的にActor分離を考える」というアプローチが取りやすくなっています。同じIsolation Domain内であればそもそもSendableを意識する必要がないので、これもSendableの必要性を減らす方向の変更と言えます。


7. まとめ

Sendableがデフォルトにならなかったのは、後方互換性やAPIコントラクトの問題が大きかったからだと思います。

ただ、それだけではなく、Swiftの設計思想として「データ競合をコンパイル時に防ぐ」ことは重要視しつつも、「全ての型にSendableを強制する」ことが正解とは考えていないように見えます。だからこそ、Region Based Isolationやsendingキーワード、モジュール単位のMainActorなど、Sendableを強制しなくても安全性を保てる仕組みをいろいろ導入しているのだと思います。

Copyableは「もともと全ての型がコピー可能だった」という歴史的事実があるからデフォルトになりましたが、Sendableにはそういう背景がありません。結果として、両者のデフォルトの考え方が異なるのは自然なことだったのかなと感じています。


参考リンク

Discussion