変数束縛の寿命を終了するconsume演算子
consume operator to end the lifetime of a variable binding
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Swiftは参照カウントとcopy-on-write(COW)によって値セマンティクスを実現しており、通常は開発者がパフォーマンスやメモリ管理を細かく気にする必要はありません。しかしパフォーマンスに敏感なコードでは、COWデータ構造の一意性を保ちたい、retain/releaseの呼び出しを減らしたい、といった要求が出てきます。
たとえば次のように Array を構築して別の関数に渡し、そのあと別の値で置き換えるコードを考えます。
func test() {
var x: [Int] = getArray()
x.append(5) // この時点で x のバッファは一意
doStuffUniquely(with: x)
x = [] // 以降は古い値を使わない
doMoreStuff(with: &x)
}
func doStuffUniquely(with value: [Int]) {
var newValue = value
newValue.append(42)
process(newValue)
}
この x は doStuffUniquely(with:) に渡したあと再代入されるだけで、古い値は二度と使いません。したがって理想的には、x の所有権(ownership)をそのまま doStuffUniquely に forward してコピーなしで渡し、さらに受け取った側も value を newValue にforwardして、一意なバッファのまま破壊的変更を行いたいところです。
コンパイラは条件が揃えばこうした最適化を自動的に行えますが、複数の解析が噛み合う必要があり、実際に効いているかは書き方に依存します。開発者としては、「この値はここで使い終わり」「以降は使わない」という意図を明示して最適化を保証させ、あとから誰かがコードを変更してその前提を壊したときにはコンパイルエラーで気づけるようにしたい、という要求があります。
また、Swiftには「local変数のライフタイムをスコープで短くする」ことは以前から可能でしたが、スコープだけに頼るとネストが深くなり、ライフタイムが交互に重なる場合や、関数パラメータ・inout パラメータのライフタイムを途中で終わらせたい場合には対応できません。
02 どのように解決されるのか
新しく consume 演算子を導入し、ローカルの let / var や関数パラメータといった 静的なライフタイムを持つbinding の現在の値をその場で消費(consume)してライフタイムを終わらせられるようにします。consume x はランタイム上は単に x の現在の値に評価されますが、コンパイル時にはその時点で x の所有権がbindingから転送されたものとして扱われ、以降で x を参照するコードはエラーになります。
func test() {
var x: [Int] = getArray()
x.append(5)
doStuffUniquely(with: consume x) // ここで x のライフタイム終了
x = [] // var なので再代入すればまた使える
doMoreStuff(with: &x)
}
受け側でも、consuming パラメータとして受け取った値をさらに別のローカル変数に consume でforwardすることで、コピーを挟まずに一意なバッファを引き継いで変更できます。
func doStuffUniquely(with value: consuming [Int]) {
var newValue = consume value // value からバッファを奪って newValue に引き継ぐ
newValue.append(42)
process(newValue)
}
あとから保守でうっかり consume のあとに元のbindingを使うコードを足してしまっても、コンパイラが検出してくれます。
func test() {
var x: [Int] = getArray()
x.append(5)
doStuffUniquely(with: consume x)
// ERROR: x used after being consumed
doStuffInvalidly(with: x)
x = []
doMoreStuff(with: &x)
}
consume できるbinding
consume のオペランドは 静的なライフタイムを持つbinding への参照でなければなりません。具体的には次のいずれかです。
- 直接囲んでいる関数のローカル
let - 直接囲んでいる関数のローカル
var - 直接囲んでいる関数のパラメータ
mutatingまたは__consumingメソッドのself
さらに、そのbindingが次のいずれかに当てはまる場合は consume できません。
@escapingクロージャやネストした関数にキャプチャされている- property wrapperが適用されている
get/set/didSet/willSet/_read/_modifyなどのアクセサが付いているasync letである
computed propertyや格納プロパティへの consume は将来の拡張として残されています(後述)。
bindingに対する操作であること
consume は値ではなくbindingに対して働きます。同じ値を別のbindingに束ねておけば、元のbindingを consume したあとでもそちらは使い続けられます。
func f() {
let x: SomeClassType = ...
useX(x)
let other = x // other は x のライフタイムを延長する新しいbinding
_ = consume x // x のライフタイムはここで終了
useX(other) // other はまだ使える
useX(other) // other もまだ使える
}
もちろん other のほうも独立に consume でき、それぞれのbindingに対して個別に診断が行われます。
フロー依存の解析と var の再初期化
解析はフロー依存です。条件分岐の一方だけで consume した場合、その分岐の後ろでだけ使えなくなります。
if condition {
let y = consume x
useX(x) // ERROR: Use after consume
} else {
useX(x) // OK
}
useX(x) // ERROR: 両方のパスで x が生きているとは限らない
var の場合は、consume したあとに再代入すれば再び使えるようになります。
if condition {
_ = consume x
useX(x) // ERROR
x = newValue // 再初期化
useX(x) // OK
} else {
useX(x) // OK(このパスでは consume していない)
}
useX(x) // OK(if で再初期化済み、else では consume していない)
inout パラメータ
inout パラメータも consume できます。解析は var と同様ですが、関数から戻るとき(return でも throw でも)にはパラメータに値が入っていなければならないため、consume したあとは関数を抜ける前に必ず再代入する必要があります。
func f(_ buffer: inout Buffer) { // error: 'buffer' not reinitialized after consume!
let b = consume buffer
b.deinitialize()
// ... buffer に再代入せずに return してしまうとエラー
}
defer を使って、早期リターンや throw を含むすべての経路で再初期化するのが定石です。
func f(_ buffer: inout Buffer) {
let b = consume buffer
defer { buffer = getNewInstance() }
try b.deinitializeOrError()
// ...
}
戻り値を捨てるときは _ = を明示する
consume はbindingのライフタイムを終わらせるだけでなく、現在の値を評価結果として返します。戻り値を使わずに consume x と書くと、戻り値のある関数呼び出しを捨てたときと同様に警告になります。値を単に破棄したいときは _ = consume x と明示的に書きます。
構文上の注意
consume は文脈依存のキーワードです。consume という名前の関数やプロパティと衝突しないよう、オペランドは識別子から始まる必要があり、識別子またはpostfix式に限定されます。
consume x // OK
consume [1, 2, 3] // consume という property への subscript アクセスとして解釈
consume (x) // consume という関数の呼び出しとして解釈
consume x.y.z // 構文上はOK
consume x[0] // 構文上はOK
consume x + y // (consume x) + y として解釈
03 今後の見通し
以下は今回のスコープ外として今後の検討対象に挙げられている方向性です。あくまで構想であり、実現を約束するものではありません。
動的なライフタイムを持つbindingへの consume
エスケープしたローカル変数やクラスの stored property のように、ライフタイムが動的に決まるbindingに対しても consume を使えるようにすることが検討されています。これらはグローバル変数や stored property のexclusivityチェックと同様にランタイムでの強制が必要になるため、コンパイル時に保証できる現在の consume とは別の構文で表現する案が示されています。
Optional のように「値なし」の状態を持つ型では、現状の consume を使って中身を奪い取りつつ自身を空状態に戻すAPIを書くことができます。
extension Optional {
mutating func take() -> Wrapped {
switch consume self {
case .some(let x):
self = .none
return x
case .none:
fatalError("trying to consume an empty Optional")
}
}
}
frozen な struct やタプルのフィールド単位の consume
@frozen な struct やタプルのようにレイアウトがコンパイル時に分かる集約型では、フィールドごとに独立に consume を許す解析の拡張が検討されています。
struct TwoStrings {
var first: String
var second: String
}
func foo(x: TwoStrings) {
use(consume x.first)
// ERROR! part of x was consumed
use(x)
// OK, this part wasn't
use(x.second)
}
deinit を持つ move-only 型の分解メソッド
将来導入が検討されている、独自の deinit を持つ move-only 型では、値が consume されたタイミングで deinit が走るのが基本動作になります。しかし、ファイルディスクリプタを呼び出し側に返して自動管理を打ち切るメソッドのように、deinit を走らせずに値を分解したいケースもあります。そうした用途のために、その型自身のメソッド内でのみ使える「deinit を抑止しつつ値を分解する」演算子(たとえば (deinit self).fd のような表記)を追加する案が示されています。Rust の mem::forget と異なり、型自身のメソッドに限定し、library evolution の制約も踏まえて慎重に設計する想定です。
computed property や property wrapper への consume
computed property、didSet / willSet を持つ property、property wrapper が適用された property などに対しても consume を使えるようにする方向性が示されています。フルにパフォーマンスを引き出すためには、self を consume して値を返す「consuming getter」や、再代入時に self を初期化し直すイニシャライザのような新しいアクセサを設計する必要があると述べられています。
implicit copy を抑制する一連の機能
consume は、implicit copy を抑制するための一連の機能の一部に位置づけられています。今後の方向性として次のようなものが挙げられています。
borrow演算子: 共有可能なグローバル変数などを、デフォルトの防御的コピーなしで関数に渡せるようにする演算子。@noImplicitCopy属性: 特定の binding、型、スコープに対して implicit copy 自体を禁止する属性。
borrow / consume パラメータ修飾子
Swift は現状、値渡しと inout の区別しか明示できず、値渡しの内部での「borrow」「consume」のどちらの呼び出し規約を使うかは実装に委ねられています。これを開発者が明示できるようにするため、func foo(x: consuming T) のように、パラメータ側で受け取り方を指定できる修飾子を導入する計画が述べられています。move-only 型ではこの区別が意味的にも重要になります。