Swift Digest
SE-0538 | Swift Evolution

Disconnected

Disconnected

Proposal
SE-0538
Authors
Franz Busch
Review Manager
Holly Borla
Status
Active review (July 17 - July 31, 2026)

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

SE-0414 で導入された region ベースの isolation は、制御フローを追跡する診断によって、non-Sendable な値が disconnected な region にあるかどうかを判定し、そうした値を isolation boundary を越えて安全に transfer できるようにしました。さらに SE-0430 の sending は、関数の引数や戻り値に「この位置の値は関数境界時点で disconnected な region にある」という契約を付け、任意の関数境界で disconnected な値をやり取りできるようにしました。

しかし sending関数境界の性質 であって、型の性質ではありません。そのため、値を stored property やコレクション、ジェネリックなコンテナに格納すると、その値が disconnected であるという情報は失われてしまいます。

たとえば、isolation boundary を越えて処理するために要素を貯めておくキューを考えます。次のような UniqueDeque に non-Sendable な値を貯めておき、取り出したものを別の isolation region へ送りたいとします。

struct UniqueDeque<Element: ~Copyable>: ~Copyable {
  func append(_ element: consuming Element) { ... }
  func popFirst() -> Element? { ... }
}

var deque = UniqueDeque<NonSendable>()
deque.append(NonSendable())

guard let element = deque.popFirst() else { return }

Task {
    print(element) // Error: element は deque と同じ isolation region にあると見なされる
}

これを成立させるには、append の引数を consume し、popFirst の戻り値を sending として返すようにすればよさそうです。しかしそうすると、この型は「disconnected な値」しか格納できなくなり、non-Sendable ではあるが disconnected ではない 値を貯めておきたい、という別の重要な用途を壊してしまいます。

根本的な問題は、sending が関数境界の性質であり、型の性質ではないことです。UniqueDeque のようなジェネリックな型は、格納した要素を disconnected に保つかどうかを条件によって選ぶことができません。appendpopFirstsending にすれば、UniqueDeque は disconnected な値専用になってしまうのです。

02 どのように解決されるのか

値が disconnected な region にあるという性質を、格納しても失われないように型として保持するためのラッパー型 Disconnected<Value> を導入します。MutexAtomic などと同じ Synchronization モジュールに追加されます。

先ほどの UniqueDeque は、要素を Disconnected でくるむことで、格納した値を disconnected なまま取り出せるようになります。

var deque = UniqueDeque<Disconnected<NonSendable>>()
deque.append(Disconnected(NonSendable()))

guard var disconnected = deque.popFirst() else { return }
let element = disconnected.take()

Task {
    print(element) // OK: element は disconnected な region にある
}

Disconnected でくるんだ値は disconnected な region に留まることが保証されます。take()Disconnected を consume して、中の値を sending として返します。返された値は disconnected なので、そのまま isolation boundary を越えて transfer できます。

Sendable であることと安全性

Disconnected<Value> は、ValueSendable かどうかにかかわらず、無条件に Sendable に適合します。中の値が常に disconnected な region にあることを保証しており、disconnected な region は安全に isolation boundary を越えて transfer できるためです。

この保証を崩さないため、値の出し入れはすべて sending 境界を経由する形に限定されています。値を取り出したり差し替えたりする操作はいずれも、ラッパーを consume するか、中の値を別の sending な値で置き換えるかのどちらかであり、ラッパー内部の記憶域への参照が transfer をまたいで生き残らないようになっています。また、すべてのメソッドが consumingmutating であるため、コンパイラの排他性チェックによって、重複したアクセスや並行アクセスも禁止されます。中の値を consume も置換もせずに覗き見るアクセサはあえて用意されていません(そうしたアクセサはラッパー内部の値への参照を外へ漏らし、無条件の Sendable 適合と両立しないためです)。

値をくるむ

init(_:) は、disconnected な region にある値を要求します。生成直後でエイリアスを持たない値は、この条件をそのまま満たします。

final class Resource: ~Sendable {}
let wrapper = Disconnected(Resource())

値を取り出して transfer する

take() はラッパーを consume して中の値を返します。返り値は disconnected な region にあるので、同じ式の中でも、いったん保持してから後でも、isolation boundary を越えて transfer できます。

final class Resource: ~Sendable {}

// wrapper はキューなど Disconnected<Resource> を保持するコンテナから
// 取り出したもので、中の Resource は周囲の文脈から disconnected である
func process(wrapper: consuming Disconnected<Resource>) async {
    let resource = wrapper.take()
    await Task.detached {
        use(resource) // OK: resource は disconnected な region にある
    }.value
}

take() が返す値に disconnected の保証が付かなければ、キャプチャした resource は呼び出し元の region の一部と見なされ、detached task でのキャプチャは許されません。take() を呼んだあとのラッパーは consume 済みで、それ以上操作できません。

値をその場で入れ替える

swap(newValue:) は、保持している値を新しい値と 1 ステップで交換します。newValue は disconnected な region にあることが求められ、返される古い値も disconnected な region にあります。

final class Resource: ~Sendable {}

func swapResources(in wrapper: inout Disconnected<Resource>) async {
    let old = wrapper.swap(newValue: Resource())
    await Task.detached {
        dispose(old) // OK: old は disconnected な region にある
    }.value
}

取り出さずに中の値を変更する

withValue(body:) は、値を取り出さずに一時的なミュータブルアクセスが必要なときに使います。クロージャは値を inout sending Value として受け取ります。

var wrapper = Disconnected([Int]())
wrapper.withValue { array in
    array.append(42)
}

inout sending は通常の inout より強い意味を持ちます。クロージャの中では値を別の isolation region へ transfer することもできます。ただし、クロージャが戻る時点でラッパーが disconnected な値を保持している状態を保たなければなりません。通常はその場での変更に使いますが、この緩い形のおかげで、値そのものを別の region へ送りたいコードとも組み合わせられます。body がエラーを throw した場合は、クロージャが最後に記憶域へ残した値をラッパーが保持したまま、エラーが呼び出し元へ伝播します。