🗝️

[Swift] KeyPathとWritableKeyPathの変換

2021/03/14に公開

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は使い道がありそうでないのが楽しいですね。

脚注
  1. private(set)で定義されていれば内部でのキャストは確実に成功するのですが、letで定義されている場合はキャストが成功しません。この点に由来するミスを防げないのは若干気がかりです。 ↩︎

Discussion