~Sendable for explicitly marking non-Sendable types
01 何が問題だったのか
公開型が Sendable に明示的に適合していないとき、その意図を読み取るのは簡単ではありません。まだ Sendable 適合を付けていないだけなのか、それとも意図的に non-Sendable にしているのかが、外から見ただけでは判別できないからです。きちんと判断するには内部のストレージ構造や同期機構の有無を理解する必要がありますが、これらはライブラリの外からは見えない実装詳細であることが多く、利用者にとっても監査する側にとっても不便でした。
加えて、クラスそのものは Sendable ではないものの、サブクラスによっては Sendable にしたい、というケースがあります。これまでも「ある型は Sendable に適合しない」ことを表す手段として、unavailable extension を書く方法はありました。
class Base {
// ...
}
@available(*, unavailable)
extension Base: Sendable {
}
ところが、unavailable な Sendable 適合はサブクラスにも継承されてしまいます。そのため、スレッドセーフなサブクラスを作ろうとしても、
final class ThreadSafe: Base, @unchecked Sendable {
// ...
}
は conformance of 'ThreadSafe' to protocol 'Sendable' is already unavailable という警告になってしまい、事実上書けません。
つまり、「このクラス自体は Sendable ではないが、サブクラス側で @unchecked Sendable を付けることは許したい」という中間的な状態を、言語として明示的に表現する方法がありませんでした。公開APIの Sendable 監査を行うライブラリ作者にとって、「Sendable が付いていないのは意図的です」と利用者に伝えつつ、サブクラスでの適合の余地を残す手段が必要でした。
02 どのように解決されるのか
~Copyable や ~Escapable と同様に、チルダ(~)プレフィックスで Sendable 適合を明示的に抑制する ~Sendable 構文を導入します。これにより、型が非 Sendable であることを意図的に宣言でき、同時に Sendable の自動推論も止めることができます。
// ExecutionResult は監査済みで、non-Sendable な associated value を
// 持つため意図的に non-`Sendable` としている。
public enum ExecutionResult: ~Sendable {
case success
// ...
case failure(NonSendable)
}
// Base 自体は non-`Sendable` だが、サブクラスは自身で
// `Sendable` を宣言できる。
public class Base: ~Sendable {
// ...
}
// ロック等で保護し、スレッドセーフにしたサブクラス。
public class ThreadSafeImpl: Base, @unchecked Sendable {
// ...
}
// mutable な non-`Sendable` 状態を持つため non-`Sendable` のまま。
public class UnsafeImpl: Base {
var x: NonSendable
}
書ける場所
~Sendable は struct / enum / class の 型宣言そのもの にのみ書けます。extension に書くと既存コードの意味が変わってしまうリスクがあるため、拒否されます。
extension Test: ~Sendable {} // Error!
プロトコル宣言やジェネリックパラメータに書くこともできません。これらはもともと Sendable 要件が暗黙に付与されるわけではないため、抑制すべき対象がないからです。
protocol P: ~Sendable {} // Error!
struct Test<T: ~Sendable> {} // Error!
func test<T: ~Sendable>(_: T) {} // Error!
他の適合と組み合わせて書くことはできます。
struct MyType: Equatable, ~Sendable {
let id: UUID
}
振る舞い
~Sendable な型は、unavailable な Sendable extension と同じく Sendable 要件を満たしません。
func processData<T: Sendable>(_ data: T) {}
struct NotSendable: ~Sendable {
let value: Int
}
processData(NotSendable(value: 42))
// error: type 'NotSendable' does not conform to the 'Sendable' protocol
一方で、unavailable extension と違って サブクラスには伝播しません。これが冒頭の問題を解消する鍵です。
class A: ~Sendable {
}
final class B: A, @unchecked Sendable {
}
func takesSendable<T: Sendable>(_: T) {}
takesSendable(B()) // Ok!
Sendable と ~Sendable の同時指定は不可
同じ型に対して Sendable と ~Sendable を無条件に両立させることはできません。アクター、グローバルアクター isolation による暗黙の Sendable、Sendable を継承したプロトコルへの適合なども対象です。
actor A: ~Sendable {} // error: cannot both conform to and suppress conformance to 'Sendable'
struct Container<T>: ~Sendable {
let value: T
}
extension Container: Sendable {} // error
protocol IsolatedProtocol: Sendable {}
struct Test: IsolatedProtocol, ~Sendable {} // error
@MainActor
class IsolatedBase {}
class Refined: IsolatedBase, ~Sendable {} // error
ただし 条件付き適合 は共存できます。「ストレージや isolation から無条件に Sendable が推論されてしまうが、本当は条件付きにしたい」という場面で便利です。
extension Container: Sendable where T: Sendable {} // Ok!
逆に、すでに条件付きで Sendable を書いている型に対しては、監査目的であっても ~Sendable を追加で書く必要はありません。
Sendable 監査への組み込み
これまで公開APIの Sendable 監査には -require-explicit-sendable フラグを使い、明示的な Sendable 適合(または unavailable extension)が無い公開型すべてに警告を出していました。このフラグが ~Sendable にも対応し、ExplicitSendable という診断グループ(デフォルトは無効)に整理されました。-Wwarning ExplicitSendable で有効化できます。
今後の展望
今回のスコープは Sendable のみですが、~ による抑制は Equatable / Hashable / RawRepresentable など、他の暗黙推論されるプロトコルにも広げる余地があると述べられています(たとえば RawRepresentable 由来の == ではなく Equatable の自動合成を使いたい enum、といった場面)。ただし、それぞれに固有の考慮事項があるため、個別のProposalで検討される想定です。