この記事は何?
SwiftのKey-Path式について、Appleの開発者向けドキュメントを独自に解説する。
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
Key-Path式
キーパス式は、型のプロパティまたは添え字を参照する。
キー・値監視などの動的プログラミングタスクでは、キーパス式を使用する。
それらは、以下の形式で記述する。
\<#type name#>.<#path#>
type name 部分はString
、[Int]
、Set<Int>
などジェネリクスのパラメータを含む具体的な型の名前。
path 部分はプロパティ名、添え字、オプショナルチェーン式、および強制アンラップ式で構成される。
キー・パスを構成するこれらのコンポーネントは、任意の順序で、必要に応じて何度でも繰り返すことができる。
コンパイル時に、キー・パス式はKeyPathクラスのインスタンスに置き換えられる。
キー・パスを使用して値にアクセスするには、あらゆる型で使用可能なsubscript(keyPath:)
添え字にキーパスを渡す。
struct SomeStructure {
var someValue: Int
}
let s = SomeStructure(someValue: 12)
// キー・パスのインスタンス
let pathToProperty = \SomeStructure.someValue
// 添え字にキー・パスを指定
let value = s[keyPath: pathToProperty]
value // 12
型が曖昧でも、コードの文脈から型推論が機能するなら、キー・パス式は型名を省略できる。
次のコードでは、\SomeClass.someProperty
の型名を省略して\.someProperty
と記述している。
class SomeClass: NSObject {
@objc dynamic var someProperty: Int
init(someProperty: Int) {
self.someProperty = someProperty
}
}
let c = SomeClass(someProperty: 10)
// observe()メソッドのパラメータは「自身の型」なので
// キー・パス式の型名を省略できる
c.observe(\.someProperty) { object, change in
// ...
}
パスはself
を参照して、キー・パスIDの(\.self)
を作成できる。
キー・パスIDはインスタンス全体を参照する。
そのため、キー・パスIDを使用して、1つのステップで変数に保存されているすべてのデータにアクセスして変更できる。
var compoundValue = (a: 1, b: 2) // ラベル付きタプル
compoundValue[keyPath: \.self] = (a: 10, b: 20)
compoundValue // (a: 10, b: 20)
パスに、いくつかのプロパティ名をピリオド.
で連結して記述すると、「プロパティの値になっているプロパティ」を参照できる。
例えば、キー・パス式 \OuterStructure.outer.someValue
は、OuterStructure
型にあるouter
プ
ロパティのsomeValue
プロパティにアクセスする。
struct OuterStructure {
var outer: SomeStructure
init(someValue: Int) {
self.outer = SomeStructure(someValue: someValue)
}
}
let nested = OuterStructure(someValue: 24)
nested.outer // SomeStructureのインスタンス
let nestedKeyPath = \OuterStructure.outer.someValue
let nestedValue = nested[keyPath: nestedKeyPath]
// nestedValue is 24
添え字に指定するパラメータがHashable
プロトコルに準拠している限り、パスには、括弧[]
を使用して添え字を含めることができる。
例えば、キー・パスの添え字を使用して、配列の2番目の要素にアクセスする。
// 配列はHashable
let greetings = ["hello", "hola", "bonjour", "안녕"]
let myGreeting = greetings[keyPath: \[String].[1]]
// myGreeting is 'hola'
添え字の値には、名前付き値またはリテラルを指定できる。
値は、値セマンティクスを使用してキーパスにキャプチャされる。
次のコードは、キー・パス式とクロージャの両方で変数index
を使用して、greetings
配列の3番目の要素にアクセスする。
index
が変更されると、キー・パス式は3番目の要素を参照するが、クロージャは新しいインデックスを使用する。
var index = 2
// 「文字列を要素とする配列」の3つ目の要素を参照するキー・パス
let path = \[String].[index]
// 「文字列の配列を受け取って、3つ目の文字列を返すクロージャ式
let fn: ([String]) -> String = {
strings in strings[index] // クロージャ内でindexを使用
}
// どちらも同じく、3つ目の要素を参照
greetings[keyPath: path] // "bonjour"
fn(greetings) // "bonjour"
index += 1 // indexを更新
// 'index'を更新しても、'path'には影響しない
// 一方、'fn'クロージャでの'index'は更新した後の値を使う
greetings[keyPath: path] // "bonjour"
fn(greetings) // "안녕"
パスでは、オプショナルチェーンと強制アンラップを使用できる。
次のコードは、キー・パス式のオプションのチェーンを使用して、オプショナルな文字列プロパティにアクセスする。
// greetings配列の先頭"hello"は5文字
let firstGreeting: String? = greetings.first
firstGreeting?.count as Any // "Optional(5)"
// 上と同じことをする
let count = greetings[keyPath: \[String].first?.count]
count as Any // "Optional(5)"
キー・パスのコンポーネントを組み合わせて、型で深くネストされた値にアクセスできる。
次のコードは、コンポーネントを組み合わせたキーパス式を使用して、配列の辞書のさまざまな値とプロパティにアクセスする。
// 「要素が辞書」の配列
let interestingNumbers = ["prime": [2, 3, 5, 7, 11, 13, 17],
"triangular": [1, 3, 6, 10, 15, 21, 28],
"hexagonal": [1, 6, 15, 28, 45, 66, 91]]
interestingNumbers[keyPath: \[String: [Int]].["prime"]] as Any // "Optional([2, 3, 5, 7, 11, 13, 17])"
interestingNumbers[keyPath: \[String: [Int]].["prime"]![0]] // "2"
interestingNumbers[keyPath: \[String: [Int]].["hexagonal"]!.count] // "7"
interestingNumbers[keyPath: \[String: [Int]].["hexagonal"]!.count.bitWidth] // "64"
通常は関数かクロージャを提供するコンテキストで、キー・パス式を使用できる。
具体的には、ルートがSomeType
型で、パスが(SomeType) -> Value
型の関数またはクロージャの代わりに、Value
型の値を生成するキー・パス式を使用できる。
struct Task {
var description: String
var completed: Bool
}
var toDoList = [
Task(description: "Practice ping-pong.", completed: false),
Task(description: "Buy a pirate costume.", completed: true),
Task(description: "Visit Boston in the Fall.", completed: false),
]
// Both approaches below are equivalent.
let descriptions = toDoList.filter(\.completed).map(\.description)
let descriptions2 = toDoList.filter { $0.completed }.map { $0.description }
キー・パス式の副作用は、式が評価される時点でのみ評価される。
たとえば、キーパス式の添え字内で関数呼び出すと、キー・パスが使用されるたびにではなく、式の評価の一部として関数が1回だけ呼び出される。
func makeIndex() -> Int {
print("Made an index")
return 0
}
// The line below calls makeIndex().
let taskKeyPath = \[Task][makeIndex()]
// Prints "Made an index"
// Using taskKeyPath doesn't call makeIndex() again.
let someTask = toDoList[keyPath: taskKeyPath]
Objective-C APIと対話するコードでキーパスを使用する方法の詳細については、SwiftでのObjective-Cランタイム機能の使用を参照。
キー値コーディングとキー値監視の詳細については、キー値コーディングプログラミングガイドおよびキー値監視プログラミングガイドを参照。
key-path式の文法
キー・パス式 → \ type? . key-path-components
キー・パスの構成 → key-path-component | key-path-component . key-path-components
キー・パスの各部品 → identifier key-path-postfixes? | key-path-postfixes
キー・パスの接尾辞 → key-path-postfix key-path-postfixes?
キー・パスの各接尾辞 → ? | ! | self | [ function-call-argument-list ]