🗝️
[Swift] KeyPathとWritableKeyPathの変換
KeyPathは生成する場所によって取りうる型が変わります。
struct Hoge{
let a: Int = 0
var b: Int = 1
private(set) var c: Int = 2
private var d: Int = 3
func test(){
let writableKeyPath_a: WritableKeyPath<Hoge, Int> = \.a //エラー(定数なので)
let keyPath_a: KeyPath<Hoge, Int> = \.a //可能
let writableKeyPath_b: WritableKeyPath<Hoge, Int> = \.b //可能(可変なので)
let writableKeyPath_c: WritableKeyPath<Hoge, Int> = \.c //可能(内部なので)
let writableKeyPath_d: WritableKeyPath<Hoge, Int> = \.d //可能(内部なので)
}
}
func test(){
let writableKeyPath_a: WritableKeyPath<Hoge, Int> = \.a //エラー(定数なので)
let keyPath_a: KeyPath<Hoge, Int> = \.a //可能
let writableKeyPath_b: WritableKeyPath<Hoge, Int> = \.b //可能(可変なので)
let writableKeyPath_c: WritableKeyPath<Hoge, Int> = \.c //不可能(setはprivateなので)
let keyPath_c: KeyPath<Hoge, Int> = \.c //可能
let writableKeyPath_d: WritableKeyPath<Hoge, Int> = \.d //不可能(privateなので)
}
さて、格納する全ての値の変更の後に必ず所定の処理を実行したい、といった状況を考えましょう。
それぞれの値にdidSetを付けても良いですが、ちょっと汚くなりそうです。
propertyWrapperでは変換後の処理をselfに依存させるのが難しそうですし、値の宣言のたびに行う処理を指定するのでは結局didSetの方式とあまり変わらない気もします。
そういうわけで、KeyPathを使います。以下のコードはsetterをprivateにし、必ずupdate
を通さないと値の変更ができない状態を作ったものです。
しかし先ほど確認したようにstructの外部ではWritableKeyPath
が取得できません。したがってこのままではエラーになってしまいます。
struct Fuga{
private(set) var number: Int = 0
private(set) var text: String = "Hello"
private(set) var bool: Bool = true
mutating func update<T>(_ keyPath: WritableKeyPath<Self, T>, process: (inout T) -> ()){
process(&self[keyPath: keyPath])
//updateの後処理を行う
someProcess()
}
}
func test(){
var fuga = Fuga()
fuga.update(\.number){value in //setterがprivateなため、WritableKeyPathが取れず、エラー
value += 42
}
}
そこでupdate
を以下のように定義することで解決することができます。
mutating func update<T>(_ keyPath: KeyPath<Self, T>, process: (inout T) -> ()){
if let keyPath = keyPath as? WritableKeyPath{
process(&self[keyPath: keyPath])
//updateの後処理を行う
someProcess()
}
}
実は実行時にはWritableKeyPath
へのキャストが成功し、無事に書き込み可能な形に変換できます。getterは外部にも公開されているので、関数の引数としてはKeyPath
を受け取りつつ、内部でキャストを行う形にすれば良いわけです[1]。
こうすればめでたく以下のコードが実行できます。
func test(){
var fuga = Fuga()
fuga.update(\.number){value in //KeyPathなら取れるのでエラーにならない
value += 42
}
}
実用性はともかく、シンプルでイケてそうな見た目になりました。KeyPathは使い道がありそうでないのが楽しいですね。
-
private(set)
で定義されていれば内部でのキャストは確実に成功するのですが、let
で定義されている場合はキャストが成功しません。この点に由来するミスを防げないのは若干気がかりです。 ↩︎
Discussion