Identity key path
01 何が問題だったのか
key path は、特定のインスタンスに依存せずに値の一部やオブジェクトグラフ上の経路を表現する仕組みです。「値の一部」を参照できるのであれば、同じように「値全体」を指せたほうが自然です。
たとえば、状態値を保持してその変更を observer に通知するようなコーディネータを考えてみます。
class ValueController<T> {
private var state: T
private var observers: [(T) -> ()]
subscript<U>(key: WritableKeyPath<T, U>) {
get { return state[keyPath: key] }
set {
state[keyPath: key] = newValue
for observer in observers {
observer(state)
}
}
}
}
このような API では、state の一部分だけでなく 値全体を一度に差し替える 操作にも同じインターフェイスを使いたくなります。しかし、これまでは key path で「値全体」を指す方法がなく、値全体を更新するためだけに別の API を用意する必要がありました。
02 どのように解決されるのか
値全体を指す identity key path を導入します。これは key path が適用される入力値そのものを参照するもので、\.self と書きます。
Swift ではどの値にも、その値全体を指す擬似プロパティ .self があります。
var x = 1
x.self = 2
print(x.self) // prints 2
identity key path は、この self メンバに対応する key path として綴られます。
let id = \Int.self
x[keyPath: id] = 3
print(x[keyPath: id]) // prints 3
struct Employee {
var name: String
var position: String
}
func updateValue(of vc: ValueController<Employee>) {
vc[\.self] = Employee(name: "Cassius Green", position: "Power Caller")
}
型は WritableKeyPath<T, T> で、ミュータブルな値に対しては書き込みにも使えます(イミュータブルな参照を書き換える用途には当然使えません)。
他の key path API との整合
identity key path は他の key path API との組み合わせでも期待通りに振る舞うよう定義されます。
-
appending(path:)で identity key path を連結すると、もう一方の key path と等しくなります。kp.appending(path: \.self) // == kp (\.self).appending(path: kp) // == kp -
MemoryLayout.offset(of:)に渡すとオフセットは0になります。Unsafe(Mutable)Pointer<T>からオフセット 0 で読み書きすることは、値全体を読み書きすることと等価だからです。MemoryLayout<Int>.offset(of: \.self) // == 0
Cocoa KVC との対応
Cocoa の KVC 互換のため、identity key path は KVC の @"self" キーパスに対応付けられます。Objective-C 側で self を指すキーパスと相互運用できます。