Edited at

Swift4のKeyPathを学ぶ


はじめに

KeyPathはSwift4から導入されました。

同じくSwift4から導入されたKVOのコードとともに見ることもあり、これは何だ?思った記憶があります。その時はKVOのクロージャ採用【Qiita記事参照】に気を取られていてあまり深く考えなかったのですが、今回 try! Swift 2019でKeyPathの発表1があったため、改めて資料を参考にしながらXcodeで動かしてみました。


環境

Xcode10.1

Swift4.2


KeyPathの定義

KeyPathの公式ドキュメントはこちらです。

https://developer.apple.com/documentation/swift/swift_standard_library/key-path_expressions

https://developer.apple.com/documentation/swift/keypath

これによるとKeyPathには、いくつかの種類があります。

Class
Declaration

KeyPath
class KeyPath<Root, Value> : PartialKeyPath<Root>
(リードオンリー)

PartialKeyPath
class PartialKeyPath<Root> : AnyKeyPath
(リードオンリー)KeyPath のsuper class

AnyKeyPath
class AnyKeyPath
(リードオンリー)PartialKeyPath のsuper class

WritableKeyPath
class WritableKeyPath<Root, Value> : KeyPath<Root, Value>
値の読み書きをサポートするキーパス。

ReferenceWritableKeyPath
class ReferenceWritableKeyPath<Root, Value> : WritableKeyPath<Root, Value>
参照セマンティクスを使用して値の読み書きをサポートするキーパス。

下記の(1)ようにKeyPathを記述するとUserのnameにアクセスできます。


Swift

    func testKeyPathBasic2()

{
struct User {
var name: String
}
var player = User(name: "Lupin")
let namePath = \User.name // <--- (1)
print(String(describing: type(of: namePath))) // WritableKeyPath<User, String>
player[keyPath: namePath] = "Goemon" // <--- (2)

XCTAssertTrue(player.name == "Goemon")
}


ここで(1)のnamePathの型を見ると KeyPath<User, String> ではなく、WritableKeyPath<User, String> であることが分かります。

KeyPathはリードオンリーのため、WritableKeyPathにしないと(2)でエラーになるからです。


KeyPath Composition

KeyPathは下記のようにパスを構成することができます。これによって複雑な構造体の変数にアクセスすることもできます。


Swift

    func testKeyPathBasic3()

{
struct User {
var name: Name
}
struct Name {
var first: String
var last: String
}

let player = User(name: Name(first: "Lupin", last: "the 3rd" ))
let namePath = \User.name
let firstPath = \Name.first
let firstNamePath = namePath.appending(path: firstPath) // ここ

let firstName = player[keyPath: firstNamePath]

XCTAssertTrue(firstName == "Lupin")
}


上記のパスの連結はそもそも下記のように書けます。


Swift

        let firstNamePath2 = \User.name.first  // パスを記述する

let firstName2 = player[keyPath: firstNamePath2]


KeyPathの応用

try! Swift 2019で取り上げられたKeyPathの手法に関しては正直メリットがそれほど伝わってきませんでした。(英語理解力不足もありますが)

しかしながら下記のようなKeyPathを使った拡張は有効だと思います。


Swift

enum SortOrder

{
case ascending
case descending
}

extension Sequence
{
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>, order: SortOrder = .ascending) -> [Element] {
return sorted { a, b in
switch order
{
case .ascending:
return a[keyPath: keyPath] < b[keyPath: keyPath]
case .descending:
return a[keyPath: keyPath] > b[keyPath: keyPath]
}
}
}
}


このsortedメソッドは、Comparableな型の変数のKeyPathでソートします。


Swift

        struct User {

var name: Name
}
struct Name {
var first: String
var last: String
}

let person1 = User(name: Name(first: "Lupin", last: "the 3rd" ))
let person2 = User(name: Name(first: "Goemon", last: "the 13th" ))


User、Nameの変数でソートする場合は、通常は下記のようにsort関数を用います。


Swift

        var people = [person1, person2]

let newPeople = people.sort{ $0.name.first > $1.name.first }

KeyPathを使用した場合は下記のようになります。


Swift

        let people = [person1, person2]

let newPeople = people.sorted(by: \User.name.first)

KeyPathを用いると

1)対象になる配列はletで良い

2)sorted()という関数になっている

3)上記関数の引数がKeyPathでより内容が明快

という違いがあります。

軽微な違いではありますが、より可読性が高いコードとなっていると思います。


参考

このような拡張を行うライブラリーとしてKeyPathKitがあります。

まだ使用してなくて感想は書けませんが、ソースコードを読む限りでは同じような方向を向いた使いやすいライブラリーのようです。





  1. try! Swift 2019で取り上げられた「Keypath入門」