Swift 6の `sending` キーワードについて調べた

Swift 6言語モードに移行を進めるにあたって、引数に sending がついているから Sendable にしようみたいな対応がある。 ただ sending のことをちゃんと説明できる自信がなかったので自分で理解できる範囲で調べた。

sending キーワード

  • 提案自体は SE-0430: sending parameter and result values で行われた
    • SE-0414: Region based Isolation がベースにあって、sending はその拡張。SE-0414の提案で region isolation (領域分離)の概念が入った
      • データが属する isolation の追跡ができるようになり
        • Sendableじゃない型でもactor boundaryを越えて送信可能になったという理解
      • Region based Isolation についてはインターネットにいろいろ書いてある
  • 関数の引数/戻り値に sending という明示的なアノテーションを付けることで、その値がactor boundaryを越えて送信可能( disconnected )であることを保証する挙動を定義できる
    • 別のactorに渡せるという言い方もできる?
  • 要はSendableであることを保証した上で、その値を安全を送信する
    • region が完全に分離されている(disconnected)なら、non-Sendableでも大丈夫ということがコンパイラで判断できる

触ってみないとわからない

Swift 6.0.3で確認した。

// swift --version
swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
Target: arm64-apple-macosx15.0

まず、non-Sendableなclassを用意する。

class NonSendable {
     var value: Int
     init(_ value: Int) {
         self.value = value
     }
 }

NonSendableを sending で受け取る関数を作る。

 actor SomeActor {
     func doSomething(_ nonSendable: sending NonSendable) async {
         print("nonSendable value:", nonSendable.value)
     }
 }

簡単な確認だけど、他参照なしであればnon-Sendableなclassでも引数に渡すことができるし、他の参照がある(regionを超えている)とnon-Sendableを渡すことができない。

struct SendingCheck {
     static func main() async {
         let actor = SomeActor()
 
         // その場で新しいインスタンスを渡す(他参照なし)はOK
         await actor.doSomething(NonSendable(1))
 
         let nonSendable = NonSendable(2)
         await actor.doSomething(nonSendable)
 
         // sendingで渡した後に参照していると `Sending 'nonSendable' risks causing data races` エラーになる
         print("nonSendable value:", nonSendable.value)
     }
 }

戻り値につけることもできる。それはそうかという感じがする。内部でプロパティとして持ちつつ sending をつけたら怒られた。その返り値がどこからもアクセスされないことを意図したい時に使えるということだろう。

actor SomeActor {
     var nonSendable: NonSendable?
     
     func getSending(value: Int) -> sending NonSendable {
         nonSendable = NonSendable(value)
         // Sending 'self.nonSendable' risks causing data races
         return nonSendable!
     }
 }

よく出くわすのは CheckedContinuationfunc resume(returning value: sending T) でProposalでも例としてあげられている。あとは AsyncThrowingStreamfunc yield(_ value: sending Element) 。だいたいはSendableにして渡すようにすれば警告(Swift 5言語モードなら)やエラーは解消するが、なぜそうなるかをわかっていると少しわかった気になれる。

まとめ

ぐっと考えると理解できるような気がするが、完全に理解した上でここでは sending つけたいな〜と思いながらコードを書くのは難しい気がする。そのうち領域展開できそう。

参考