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 らしいコードが記述できそうです。