noncopyable型のためのborrowing / consumingパターンマッチング
Borrowing and consuming pattern matching for noncopyable types
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
SE-0390 で ~Copyable な struct / enum(non-copyable 型)を宣言できるようになりましたが、switch 文での扱いには大きな制約がありました。non-copyable な値に対する switch は常に consuming 扱いで、パターンマッチが走った時点で値は消費され、マッチ後には同じ値を再び使えなくなってしまうのです。
これはとくに non-copyable な enum の表現力を大きく損ねます。enum の関連値にアクセスする手段は switch のパターンマッチしかなく、それが常に値を消費してしまうと、たとえば「状態を確認したいだけ」「関連値を覗き見たいだけ」といった用途でも元の値を手放さざるを得ません。
struct NC: ~Copyable {}
enum NoncopyableEnum: ~Copyable {
case copyable(Int)
case noncopyable(NC)
}
var foo: NoncopyableEnum = ...
switch foo { // SE-0390 時点ではエラー。consume foo と明示しないと通らない
case .copyable(let n):
print(n)
case .noncopyable(let x):
// ...
}
// foo はこの switch で消費されてしまい、以降使えない
必要だったのは、non-copyable な値を借りたままパターンマッチできる仕組みと、パターンごとにどの所有権モード(借りるのか、消費するのか)で動くかを自然に決められるモデルです。
02 どのように解決されるのか
non-copyable な値に対する switch は、もはや常に consuming ではなく、対象式とパターンの組み合わせから所有権モードを推論するようになります。加えて、パターン中に borrowing なバインディングを明示的に書けるようにし、所有権の意図をパターン側から制御できるようにします。
switch の3つの所有権モード
switch の振る舞いは次の3つのいずれかに分類されます。右に行くほど厳しい(強い所有権を要求する)モードです。
- copying: 対象はセマンティクス上コピーされ、必要に応じてさらに複製も作られる。copyable な値に対するベースラインです。
- borrowing: 対象は
switchブロックの間だけ借りられる。 - consuming: 対象は
switchブロックで消費される。
コンパイラはパターンを再帰的に見て、より厳しい所有権を要求するパターンが現れるたびにモードを強めていきます。
ベースラインモードの決まり方
non-copyable な対象のベースラインは、対象式の形で決まります。
- 変数や stored property(および実験的な
_read/_modify/unsafeAddressアクセサを持つプロパティ・サブスクリプト)を参照していて、かつconsume演算子で明示的に消費していない場合は borrowing。 - それ以外(関数の戻り値などの一時値)は consuming。
copyable な型が baseline copying なのは従来と同じです。
パターンと所有権モードの例
copyable な enum の場合、let バインディングはすべて copying のままです。
enum CopyableEnum {
case foo(Int)
case bar(Int, String)
}
case let x: // copying
case .foo(let x): // copying
case .bar(let x, let y): // copying
non-copyable な enum の場合、ベースラインと各 let バインディングの型に応じてモードが決まります。
struct NC: ~Copyable {}
enum NoncopyableEnum: ~Copyable {
case copyable(Int)
case noncopyable(NC)
}
var foo: NoncopyableEnum // stored variable
switch foo {
case let x: // borrowing
case .copyable(let x): // borrowing(x: Int は copyable)
case .noncopyable(let x): // borrowing
}
func bar() -> NoncopyableEnum { ... } // 一時値を返す
switch bar() {
case let x: // consuming
case .copyable(let x): // borrowing(x: Int は copyable で、対象の一部だけ取り出せば済む)
case .noncopyable(let x): // consuming
}
SE-0390 時点では switch foo のような書き方はエラーでしたが、本提案では上記のとおり borrowing として正しく動作します。これまでのように switch consume foo と明示すれば、従来どおり consuming として振る舞わせることもできます。
マッチ中に対象を mutate / consume できない
switch のパターン評価順は未規定で、マッチが最終的に決まるまでは「どのケースが当たったか」が確定しません。non-copyable な型はコピーで逃げられないので、マッチが確定するまで対象を mutate したり消費したりすることはできません。
多くのパターン(enum ケースや、将来的に対応予定の non-copyable タプルなど)は、「条件を判定する部分」と「バインディングを作るために値を消費する部分」を分けて扱えるため、let / var バインディングを含んでいても問題なく動きます。
extension Handle {
var isReady: Bool { ... }
}
let x: MyNCEnum = ...
switch consume x {
// 複数ケースに `let y` があっても、マッチが確定してからバインディングのための
// 消費が行われるので OK
case .foo(let y) where y.isReady:
y.close()
case .foo(let y):
y.close()
}
一方、where 節ではバインディングを消費できません。where の評価後にマッチが失敗しうる以上、その時点で値を消費すると後続のケースに値が残らなくなるためです。case 本体では消費できます。
extension Handle {
consuming func tryClose() -> Bool { ... }
}
switch consume x {
// error: `where` 節では `y` を消費できない
case .foo(let y) where y.tryClose():
y.close()
case .foo(let y):
y.close() // 本体での消費は OK
}
同じ理由で、~= 演算子が対象を消費するような式パターンも non-copyable な対象には使えません。
extension Handle {
static func ~=(identifier: Int, handle: consuming Handle) -> Bool { ... }
}
switch consume x {
// error: マッチ確定前に対象を消費する `~=` は使えない
case .foo(42):
...
case .foo(let y):
...
}
なお、non-copyable 型はまだ動的キャストに対応していませんが、将来 is T / as T を受け入れるときも同じ方針になります。is T は borrowing のまま判定でき、as T は消費を伴うため consuming な switch に限られ、かつ p as T の中の p は irrefutable で where 節を持てない、といった制約が付く見込みです。
case 条件(if / while / for / guard)
if case <pattern> = <subject> などの case 条件は、「そのパターンだけを持ち、マッチ成功がバインディング付きの case、失敗が default に対応する switch」と考えれば同じ規則で動きます。対象への所有権の取り方も、そのパターンの要求に従います。
ソース互換性
SE-0390 は non-copyable 変数への switch に consume 演算子を要求していました。この書き方は引き続き動きます(ただし実際のマッチが borrowing で済むケースでも、バインディングの寿命は consume したタイミングで終わる点が従来の実装と若干異なります)。enum にはまだ deinit を書けず、non-copyable タプルも未対応、deinit を持つ struct は部分消費できず全体として消費するしかない、といった現状から、実挙動への影響はほぼ無いと見込まれています。
また、従来は理論上は consuming な ~= や where 節での消費が可能でしたが、本提案ではこれらを明確に禁止します。既存実装でもこれらを実用に乗せるのは難しかったため、実害は無いと判断されています。
03 今後の見通し
本提案の延長として、次のような方向性が示されています。いずれも将来の構想であり、実現を約束するものではありません。
inout パターン
本提案で追加された borrowing と consuming に加えて、パターンから排他参照を取って値の一部をその場で mutate できる mutating な switch モードを導入する案です。パターン中に inout バインディングを書けるようにし、所有権の強さとしては borrowing と consuming の中間に位置づけられます。
let バインディングの自動借用と明示的な consuming バインディング
copyable な型では、let や var が形式的には独立したコピーを束縛するように見えても、コンパイラがコピーを最適化して元の値をその場で借用することがあります。同じ発想を non-copyable な型の let パターンバインディングにも広げ、消費が必要でなければ自動で borrow する「do what I mean」的な挙動に揃える案です。すでに動いているコードはそのまま動きつつ、より多くのコードがコンパイルできるようになる後方互換な変更になる見込みです。
合わせて、所有権を明示的に制御したい場合に向けて、パターン中に consuming バインディングを書けるようにする案も挙げられています。consuming バインディングは暗黙コピーされず、対象が copyable であっても switch のモードを consuming に格上げします。
enum の deinit
SE-0390 では、non-copyable 型に対する switch が consuming に限られていたことから、enum には deinit を書けないようになっていました。deinit を持つ non-copyable 型は分解できず、consuming switch でしか触れない enum ではそもそも何もできなくなってしまうためです。本提案で borrowing な switch が可能になったことで、enum にも deinit を許す余地が生まれました。ただし consuming switch で分解すると deinit を迂回してしまうので、deinit を持つ enum は consuming な分解を禁止する、といった制約付きになる見込みです。
明示的な borrow 演算子
borrow 演算子を使って、switch の対象を明示的に借用として扱えるようにする案です。対象が copyable であったり、一時値であったりしても、コピーや消費を抑えて借用にできるようになります。
let x: String? = "hello"
switch borrow x {
case .some(let y): // y は x からの借用として束縛される(コピーなし)
...
}
パターン中の borrowing バインディング
関数のローカル束縛や nonescapable 型のフィールドとして borrowing / inout バインディングが導入された後、パターン中にも明示的な borrowing バインディングを書けるようにする案です。non-copyable な borrowing switch のパターンでは let バインディングがすでに借用になっているので主な用途は別にあり、copyable なバインディングに対して暗黙コピーを抑える指定として使うことが想定されています。