8
5

More than 3 years have passed since last update.

【Swift】キーパスについて

Last updated at Posted at 2021-03-16

キーパスとは

オプショナルチェーンのようにさまざまなインスタンスが互いに参照しあう関係にあった時、あるインスタンスを起点として次々に参照をたどって別のインスタンス参照できることがありますが、そのような経路をインスタンスとして保存したり、関数の引数と指定渡したりすることです。

以下は、ある学生sがクラブに所属している場合、そのクラブに顧問の先生がいればその先生の名前を表示するためのオプショナルチェーンによる記述です。

if let name = s.club?.teacher?.name {
    print(name)
}

この記述は変数sに対してのみ有効ですが、Student型のインスタンスならばどれに対しても同様な参照を行うことができます。ここで、次のような記述が可能です。

let teacherNamePath = \Student.club?.teacher?.name 
if let name = s[keyPath: teacherNamePath] {
    print(name)
}

この\Studentから始まる記述がキーパス式です。

キーパス式の形式

\型名.パス

キーパス式の記述

キーパス式にはプロパティーの名前を「.」で区切って並べるほか、配列の要素を添字として指定したり、タプルの要素を通番名またはラベル名で指定することが可能です。次の例は、冒険者とパーティーのクラス定義です。

struct Explorer {
    let name: String
    var items: [String: Int]
    var ability: (cure: Int, magic: Int)?
    init(_ name: String, items: [String: Int]) {
        self.name = name
        self.items = items
    }
}

class Party {
    let name: String
    var members = [Explorer]()
    init(name: String) {
        self.name = name
    }
}

この定義を使って次のような設定を行います。

var party = Party(name: "LuckyStar")
let fighter1 = Explorer("こなた", items: ["剣": 1, "兜": 1])
let fighter2 = Explorer("つかさ", items: ["槍": 1, "薬草": 3])
var fighter3 = Explorer("かがみ", items: ["薬草": 1, "杖": 1])
fighter3.ability = (cure: 10, magic: 20)
party.members = [fighter1, fighter2, fighter3]

ここで、次のようにキーパスを指定して実行することが可能です。

let kp1 = \Party.members[1].items["薬草"]
if let n = party[keyPath: kp1] {
    print(n) // 3
}

let kp2 = \Party.members[2].ability?.1
if let a = party[keyPath: kp2] {
    print(a) // 20
}

キーパスによる値の操作

キーパスを使って、対象のプロパティの値を変更することもできます。
ここで、次のようなキーパス式を作成して型を調べてみます。

let keypath1 = \Explorer.items["短刀"]
print(type(of: keypath1)) // WritableKeyPath<Explorer, Optional<Int>>
let keypath2 = \Party.members[2].items["薬草"]
print(type(of: keypath2)) // ReferenceWritableKeyPath<Party, Optional<Int>>
let keypath3 = \Party.members[1].name
print(type(of: keypath3)) // KeyPath<Party, String>

プロパティの値を更新するにはWritableKeyPath型、またはReferenceWritableKeyPath型のキーパスを使う必要がありますが、実際に値の参照、更新ができるかどうかは具体的な変数や定数の状況によって、異なります。定数keypath1, keypath2格納されたキーパスを使って、値を更新してみます。

fighter3[keyPath: keypath1] = 1
print(fighter3.items) // ["薬草": 1, "杖": 1, "短刀": 1]
party[keyPath: keypath2]! = 5
print(party.members[2].items) // ["薬草": 5, "杖": 1]

キーパスを使った表記の意義

キーパスはデータ構造の中のある位置を静的に示し、さらにその値の参照を後から行うことができます。例えば、学生とクラブの例を考えます。クラスのインスタンスは参照のデータのため、クラブ顧問の先生とインスタンスが得られたらそれを何かの変数から直接参照するようにできます。しかしその後、別の先生がクラブ顧問になるとその変数が参照するのは顧問の先生ではなくなってしまいます。一方、情報の位置をキーパスとして指定する方法ならば、データが書き換えられても意図する情報を得ることができます。

let newArray = array.sorted { $0[keyPath: k] > $1[keyPath: k] }

変数kはキーパスで、例えば\Club.budgetや\Club.members.countを指定すると、予算の多い順、構成員の人数の多い順に並べ替えることができます。
プロパティの参照やオプショナルチェーンの記述は特定のインスタンスを起点とした式としてプログラムの中に書き込まれますが、キーパスは起点に型を指定して抽象化されているため、上記のようにコンパクトで効果的な記述を可能にします。

8
5
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
8
5