はじめに
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にアクセスできます。
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は下記のようにパスを構成することができます。これによって複雑な構造体の変数にアクセスすることもできます。
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")
}
上記のパスの連結はそもそも下記のように書けます。
let firstNamePath2 = \User.name.first // パスを記述する
let firstName2 = player[keyPath: firstNamePath2]
KeyPathの応用
try! Swift 2019で取り上げられたKeyPathの手法に関しては正直メリットがそれほど伝わってきませんでした。(英語理解力不足もありますが)
しかしながら下記のようなKeyPathを使った拡張は有効だと思います。
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でソートします。
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関数を用います。
var people = [person1, person2]
let newPeople = people.sort{ $0.name.first > $1.name.first }
KeyPathを使用した場合は下記のようになります。
let people = [person1, person2]
let newPeople = people.sorted(by: \User.name.first)
KeyPathを用いると
1)対象になる配列はletで良い
2)sorted()という関数になっている
3)上記関数の引数がKeyPathでより内容が明快
という違いがあります。
軽微な違いではありますが、より可読性が高いコードとなっていると思います。
参考
このような拡張を行うライブラリーとしてKeyPathKitがあります。
まだ使用してなくて感想は書けませんが、ソースコードを読む限りでは同じような方向を向いた使いやすいライブラリーのようです。