2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

この記事は何?

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 ]

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?