Borrow and Inout types for safe, first-class references
01 何が問題だったのか
Swiftでは、ある値への一時的なアクセスを関数呼び出しの形で受け渡すことができます。
inoutパラメータは、呼び出し側が所有する値への 一時的な排他アクセス(exclusive access) を受け取ります。呼ばれた側は値を変更でき、現在の値を consume して新しい値で置き換えることもできます。関数から戻ると、呼び出し側が所有権を取り戻します。borrowingパラメータは、呼び出し側の値への 一時的な共有アクセス(shared access) を受け取ります。同じ値への別のアクセスが並行している可能性があるため、基本的には読み取りしかできませんが、独立したコピーを作らずに値を参照できます。
これらはあくまで関数呼び出しの文脈でだけ使える仕組みでした。同じような参照を ローカル変数 として束縛したり、他の型のメンバ として保持したり、ジェネリックコンテナの要素 として扱ったりしたい場面は多くあります。例えば辞書のエントリに名前を付けて繰り返し更新する、struct-of-arrays 構造から単一要素への参照を取り出す、といったケースです。
これまで、こうした参照を表現するにはクラスで値をボックス化して共有する方法がありましたが、アロケーションや参照カウント、動的な排他性チェックといったオーバーヘッドが付いて回ります。UnsafePointer を使うこともできますが、安全でない上にSwiftの高レベルなセマンティクスとの相性が悪く、正しく扱うには細心の注意が必要です。
そのため、inout / borrowing 相当の参照を ファーストクラスの型 として扱える仕組みが求められていました。これが実現すれば、ローカル束縛・メンバ・ジェネリック引数のいずれの形でも、安全かつ低オーバーヘッドに参照を取り回せるようになります。
02 どのように解決されるのか
標準ライブラリに、他の値への参照を表す2つの non-Escapable なジェネリック型 Borrow と Inout を追加します。前者は共有された borrow、後者は排他的な inout アクセスに対応します。
public struct Borrow<Value: ~Copyable>: Copyable & ~Escapable {
@_lifetime(borrow target)
public init(_ target: borrowing Value)
public var value: Value { borrow }
}
public struct Inout<Value: ~Copyable>: ~Copyable & ~Escapable {
@_lifetime(&target)
public init(_ target: inout Value)
public var value: Value { borrow; mutate }
}
これらは SE-0446 の lifetime dependency を使ってターゲットとの寿命関係を表現し、SE-0507 で導入された borrow / mutate アクセサを使って余計な制約なしにターゲットへアクセスします。参照はイニシャライザでターゲットを渡して作り、value プロパティを通じて中身を読み書きします。
基本的な使い方
辞書のようにキー参照のたびにハッシュを辿り直すのが無駄な場面では、一度 Inout を作っておけば繰り返し書き込めます。
func updateTotal(in dictionary: inout [String: Int], for key: String,
with values: [Int]) {
var entry = Inout(&dictionary[key, default: 0])
// ハッシュを引き直さずに繰り返し更新できる
for value in values {
entry.value += value
}
}
Borrow は読み取り専用、Inout は書き込みも可能です。参照が生きている間、ターゲットは対応するアクセス下に置かれます。
var totals = [17, 38]
do {
let apples = Borrow(totals[0])
print(apples.value) // 17
apples.value += 2 // ERROR: Borrow.value は読み取り専用
totals[1] += 1 // ERROR: borrow 中は totals を変更できない
print(totals[1]) // 38(他の borrow は可能)
}
do {
var bananas = Inout(&totals[1])
bananas.value += 2
print(bananas.value) // 40
print(totals[1]) // ERROR: totals[1] は bananas に排他的に握られている
}
print(totals) // [17, 42]
これは Array と、その span / mutableSpan から作られる Span / MutableSpan の関係と同じです。実際、Borrow / Inout は単一要素版の Span / MutableSpan と捉えることができます。
参照を返す関数
lifetime dependency を付けることで、参照を返す関数も書けます。
struct Vec3 {
var x, y, z: Double
@_lifetime(&self)
mutating func at(index: Int) -> Inout<Double> {
switch index {
case 0: return Inout(&x)
case 1: return Inout(&y)
case 2: return Inout(&z)
default: fatalError("out of bounds")
}
}
}
オプショナルやジェネリックコンテナに入れることもできます。
@_lifetime(&array)
func element(of array: inout [Int], at: Int) -> Inout<Int>? {
if at >= 0 && at < array.count {
return &array[at]
} else {
return nil
}
}
non-Escapable 型のフィールドとして
Borrow / Inout は他の non-Escapable 型のフィールドにできます。これにより、struct-of-arrays から単一レコードへの参照を表す型を組み立てられます。
struct People {
var names: [String]
var ages: [Int]
subscript(i: Int) -> Person {
@_lifetime(&self)
mutating get {
return Person(name: &names[i], age: &ages[i])
}
}
}
struct Person: ~Copyable, ~Escapable {
var name: Inout<String>
var age: Inout<Int>
}
get / set 等を介したアクセスとの関係
Inout は stored property だけでなく、get / set ペア、yielding コルーチンアクセサ、動的排他性チェック下の property、didSet / willSet オブザーバ付き property なども対象にできます。この場合、Inout を作るときにアクセスが開始(getter の呼び出しやコルーチンの起動など)され、Inout の寿命が尽きるタイミングでアクセスが終了(setter の呼び出しや didSet の実行など)します。
struct NoisyCounter {
private var _value: Int
var value: Int {
get { print("counted \(_value)"); return _value }
set { print("updating counter to \(newValue)"); _value = newValue }
}
}
var counter = NoisyCounter(67)
do {
var counterRef = Inout(&counter.value) // "counted 67" を出力
counterRef.value += 1
counterRef.value += 1
// ブロック終了時に "updating counter to 69" を出力
}
このとき Borrow / Inout はアクセスに 依存する だけで、アクセスを終わらせる文脈そのものを内部に持つわけではありません。したがって、非自明なアクセス(getter/setter など)から作った参照を、その呼び出し元の外まで寿命を延ばすことはできません。
@_lifetime(&target)
func noisyCounterRef(from target: inout NoisyCounter) -> Inout<Int> {
// ERROR: Inout の寿命を正式なアクセスの外まで延ばすことになる
return Inout(&target.value)
}
一方、struct の stored property への直接アクセスや、borrow / mutate アクセサ経由のアクセスであれば、終端で実行すべきコードが無いため、参照の寿命は親アクセスにだけ縛られます。
Borrow の表現について
Borrow<Value> は、Value の性質に応じて ターゲットへのポインタ として表現されたり、ターゲットのビット単位コピー として表現されたりします。ポインタ表現になるのは次のいずれかに該当するときです。
MemoryLayout<Value>.sizeが4 * MemoryLayout<Int>.sizeより大きいValueが bitwise-borrowable ではないValueが addressable-for-dependencies である
ここで bitwise-borrowable とは、Int やオブジェクト参照のように「メモリ上のどこにあっても同じ意味を持つ」型のことです。こうした型は借用もビットコピーで渡せるため、安定したアドレスを持ちません。小さな bitwise-borrowable 値に対しては、Borrow も値そのものの表現を使うことで、借用を受け取ってそのまま Borrow として返すような関数を可能にします。
addressable-for-dependencies とは、InlineArray のように「span などを通じて内部メモリへのポインタを持つ依存値を生み出す」型のことです。これらは呼び出し規約上も常に間接渡しされ、返り値との lifetime dependency が安全に保てるようになっています。Borrow はこうした型に対してポインタ表現を使うため、Borrow を介して span 等を取り出しても問題ありません。C / Objective-C / C++ から import された struct・union・class も常に addressable-for-dependencies として扱われ、それらの言語のポインタや参照との相互運用がしやすくなっています。
Inout についてはより単純で、inout パラメータが常にアドレス渡しされるのと同じく、Inout も常にポインタ表現を使います。
今後の展望
Borrow / Inout は、ローカル参照束縛が欲しいという長年の要望に対する土台になります。ただし、型として露出している以上、value プロパティを介したアクセスなど記述上のオーバーヘッドは残るため、将来的には構文糖としての参照束縛(たとえば borrow x = y のような記法)を別途検討することが示唆されています。それでも Borrow / Inout は、ジェネリック引数として使えることや、参照そのものを変数として再代入できることなど、束縛構文では得られない表現力を持つため、言語への恒久的な追加として位置付けられています。
そのほか、
Borrow/Inoutのメンバ参照を透過的に対象へ転送する仕組み(Rust のDerefに相当するもの)- non-
Escapable型をターゲットにできるBorrow/Inout - 常にポインタ表現を取る borrow 参照型
- 任意の関数を単一 yield のコルーチンとして定義できるようにして、非自明なアクセスから参照を返せるようにする拡張
Inoutのvalue変更に現状var束縛が必要な問題を解消するexclusive所有モードや、Rust 風の reborrowing
といった方向性が将来の検討対象として挙げられていますが、いずれも speculative なアイデアであり、本Proposalのスコープ外です。