Help us understand the problem. What is going on with this article?

【Swift】KeyPath 式が生成する実際のオブジェクトについて

Swift4 で導入された KeyPath は、init ではなく、次のような \<Type>.<path> の形式でオブジェクトを生成します。

struct Foo {
    var bar: Int
}

let keyPath = \Foo.bar // WritableKeyPath<Foo, Int>

上記の場合、 WritableKeyPath に型推論されて、次のように読み書き可能です。

var foo = Foo(bar: 0)
print(foo[keyPath: keyPath]) // 0
foo[keyPath: keyPath] = 1
print(foo[keyPath: keyPath]) // 1

また、var ではなく、let で宣言した場合は、KeyPath クラスに型推論されます。

struct Foo {
    let bar: Int
}

let keyPath = \Foo.bar // KeyPath<Foo, Int>

このように、プロパティの宣言方法に応じて、下記のいずれかの型に型推論をするようです。

  • KeyPath
  • WritableKeyPath
  • ReferenceWritableKeyPath

どのように宣言すると、それぞれの型に推論されるのか?気になったので Playground で動作確認をしてみました。

環境

  • Xcode Version 10.2.1
  • Apple Swift version 5.0.1

KeyPath

読み込み専用のクラスである KeyPath<Root, Value> は、プロパティの宣言を次のようにした場合に推論されます。

  • let で宣言した定数
  • getter のみの計算プロパティ
  • { get } 宣言したプロトコルのプロパティ

let で宣言した定数

struct Foo {
    let bar: Int = 0
}

getter のみの計算プロパティ

struct Foo {
    var bar: Int {
        return 0
    }
}

get のみ宣言したプロトコルのプロパティ

protocol Foo {
    var bar: Int { get }
}

このような場合、明示的に WritableKeyPath 型に代入しようとしてもコンパイルエラーになります。

let keyPath: WritableKeyPath<Foo, Int> = \Foo.bar // コンパイルエラー

また、 WritableKeyPath へのダイナミックキャストは実行時エラーになります。

let keyPath = \Foo.bar as! WritableKeyPath<Foo, Int> // 実行時エラー

実際に、次のようにして実行時の型を調べると KeyPath 型であることが分かります

let keyPath = \Foo.bar
type(of: keyPath) // KeyPath<Foo, Int>

WritableKeyPath

読み書き両用のクラスである WritableKeyPath<Root, Value> は、プロパティの宣言を次のようにした場合に推論されます。

  • var で宣言した変数
  • getter / setter 計算プロパティ
  • { get set } 宣言したプロトコルのプロパティ

var で宣言した変数

struct Foo {
    var bar: Int = 0
}

getter / setter 計算プロパティ

struct Foo {
    var bar: Int {
        get { return 0 }
        set {}
    }
}

get set 宣言したプロトコルのプロパティ

protocol Foo {
    var bar: Int { get set }
}

このような場合、明示的に WritableKeyPath 型に代入することもできます。

let keyPath: WritableKeyPath<Foo, Int> = \Foo.bar // OK!

また、 WritableKeyPath のスーパークラスにキャストすることもできます。
次のように KeyPath として宣言することも可能です。

let keyPath: KeyPath<Foo, Int> = \Foo.bar // OK!

実行時の型を調べましたところ、型宣言の有無によらず WritableKeyPath でした。
次のように KeyPath として宣言しても、実際には WritableKeyPath 型のオブジェクトなので、
ダイナミックキャストも問題なくできます。

let keyPath: KeyPath<Foo, Int> = \Foo.bar
type(of: keyPath) // WritableKeyPath<Foo, Int>
keyPath as! WritableKeyPath<Foo, Int> // OK!

ReferenceWritableKeyPath

構造体などの値型で読み書き両用プロパティを宣言した場合は WritableKeyPath ですが、クラスで宣言した場合は ReferenceWritableKeyPath になります。

var で宣言した変数

class Foo {
    var bar: Int = 0
}

getter / setter 計算プロパティ

class Foo {
    var bar: Int {
        get { return 0 }
        set {}
    }
}

get set 宣言したプロトコルのプロパティ

protocol Foo: AnyObject {
    var bar: Int { get set }
}

プロトコルは AnyObject などでクラスに制限することで ReferenceWritableKeyPath になります。
WritableKeyPath の場合と同様に、型宣言した場合も、実際のオブジェクトは ReferenceWritableKeyPath になります。

let keyPath: KeyPath = \Foo.bar
type(of: keyPath) // ReferenceWritableKeyPath

ここまでのまとめ

プロパティの宣言方法に応じて、下記のいずれかの型に型推論されます。

  • KeyPath
  • WritableKeyPath
  • ReferenceWritableKeyPath

KeyPath に推論される場合は、実行時のオブジェクトも KeyPath で、WritableKeyPath など書き込み可能なキーパスで型宣言しようとするとコンパイルエラーになります。

WritableKeyPath に推論される場合は、実行時のオブジェクトも WritableKeyPath で、KeyPath にアップキャスト可能で、またその後 WritableKeyPath にダウンキャストすることも可能です。

クラスの場合は、読み書き可能なプロパティは ReferenceWritableKeyPath になります。

アクセス制御をした場合

Swift では、セッターのアクセスを制御して内部のみ公開することができます。

struct Foo {
    private(set) var bar: Int = 0 // getter は internal, setter は private
}

このような場合は、キーパスオブジェクトの生成場所によって、
型推論の挙動が変わりました。

  • setter にアクセスできる場合
  • getter のみアクセスできる場合

setter にアクセスできる場合
setter にアクセスできる場合は、(Reference)WritableKeyPath に型推論されます。

struct Foo {
    private(set) var bar: Int = 0

    func foo() {
        let keyPath = \Foo.bar // WritableKeyPath<Foo, Int>
    }
}

getter のみアクセスできる場合
getter のみアクセスできる場合は、KeyPath に型推論されます。

struct Foo {
    internal private(set) var bar: Int = 0
}

class FooUser {
    func use() {
        let keyPath = \Foo.bar // KeyPath<Foo, Int>
    }
}

明示的に WritableKeyPath 型を宣言してもコンパイルエラーになります。

class FooUser {
    func use() {
        let keyPath: WritableKeyPath<Foo, Int> = \Foo.bar // コンパイルエラー
    }
}

なのですが、実行時のオブジェクトは WritableKeyPath なので、ダイナミックキャストをしても実行時エラーになりません。

アクセス制御を無視して、次のように書き込みすることができます。

class FooUser {
    func use() {
        let keyPath = \Foo.bar as! WritableKeyPath<Foo, Int>
        var foo = Foo()
        print(foo.bar) // 0
        // foo.bar = 2 // コンパイルエラー
        foo[keyPath: keyPath] = 2 // WritableKeyPath 経由で書き込みができる
        print(foo.bar) // 2
    }
}

このようにアクセス制御を無視したコードを実際のプロダクトで書くことはないとは思いますが、内部で書き込みが可能なプロパティは、アクセスレベルによらず実行時のオブジェクトは WritableKeyPath になることが分かりました。

コンパイル時の検査は、アクセスする箇所に応じた KeyPath として推論してくれるので、
この仕組を利用することで Swift らしいコードが記述できそうです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away