Swift 方便存取 property 的 KeyPath & KeyPath as Functions
Swift 的 KeyPath 可以指定某個 property 或 subscript,方便我們存取資料的 property 或 subscript,接下來就讓我們以 Song 為例,看看它特別的地方吧。
struct Song {
var name: String
var genre: String
}
var favoriteSong = Song(name: "小幸運", genre: "抒情")
print(favoriteSong.name)
存取 property
我們可以用 favoriteSong.name 讀取歌曲的名字,但也可以改成以下 KeyPath 的方法讀取。
let name = \Song.name
print(favoriteSong[keyPath: name])
說明
- 描述 KeyPath
let name = \Song.name
KeyPath 的標準寫法是 \ + 型別. + property。
\Song.name
表示型別 Song 的 property name。輸入 KeyPath 時 Xcode 也會貼心地提供自動完成,因此打完 \Song. 後,它會自動列出 property genre & name。
利用 KeyPath 讀取 property。
print(favoriteSong[keyPath: name])
在 favoriteSong 後加上 [ ],然後在參數 keyPath 後傳入 name 讀取 favoriteSong 的 song。
我們也可以在參數 keyPath: 後直接傳入 \Song.name
,描述想要讀取的 property。
print(favoriteSong[keyPath: \Song.name])
甚至我們還可以省略 Song,輸入 \.name
。
print(favoriteSong[keyPath: \.name])
KeyPath 的型別
如下圖所示,name 是型別是 WritableKeyPath<Song, String>。
WritableKeyPath 表示它是可以讀取和寫入的 KeyPath。從以下 WritableKeyPath 的定義,我們看出它有兩個 generic 型別代號,Root & Value,Root 代表資料的型別,Value 代表 property 的型別,因此 \Song.name 的型別為 WritableKeyPath<Song, String>,表示從 Song 裡讀取的 name 為 String。
class WritableKeyPath<Root, Value> : KeyPath<Root, Value>
存取 subscript
KeyPath 也可以存取 subscript,例如以下 array & dictionary 的例子。
var favoriteSongs = [
Song(name: "小幸運", genre: "抒情"),
Song(name: "漂向北方", genre: "饒舌")
]
傳入 \[Song].[1]
。
var song = favoriteSongs[keyPath: \[Song].[1]]
傳入 \.[1]
。
var song = favoriteSongs[keyPath: \.[1]]
dictionary 的例子。
var favoriteSongDic = [
"田馥甄": Song(name: "小幸運", genre: "抒情"),
"黃明志王力宏": Song(name: "漂向北方", genre: "饒舌")
]
傳入 \[String: Song].["田馥甄"]
。
var song = favoriteSongDic[keyPath: \[String: Song].["田馥甄"]]
傳入 \.["田馥甄"]
。
var song = favoriteSongDic[keyPath: \.["田馥甄"]]
KeyPath as Functions
KeyPath 有很多方便的地方,比方它可以當成 function 參數傳入。
接下來我們以存取經典的文學名著說明。
struct Book {
var name: String
var character: String
var year: Int
}
var books = [
Book(name: "愛麗絲夢遊仙境", character: "愛麗絲", year: 1865),
Book(name: "湯姆歷險記", character: "湯姆", year: 1876),
Book(name: "唐‧吉訶德", character: "唐‧吉訶德", year: 1605)
]
- map
傳統寫法: 利用 map 搭配 closure 取得書本主角名稱的 array。
let characters = books.map { $0.character }
map 的參數型別是 function 型別,因此我們傳入 closure { $0.character }
。
當參數的型別是 (Root) -> Value 時,我們可以傳入 KeyPath,表示回傳的內容為 Root 型別資料下的 property。在剛剛的例子,Root 型別為 Book,因此我們可傳入 \.character
,表示回傳書本的角色名字。
let characters = books.map(\.character)
當我們想用 print 印出 array 成員的某個 property 時,KeyPath 的寫法也很方便。
- filter
KeyPath 也可以多層串聯,以下程式的 print(babies.map(\.rabbit.name))
將印出 ["彼得兔", "米菲兔"]
。
struct Rabbit {
let name: String
}
struct Baby {
let name: String
let age: Int
let rabbit: Rabbit
}
var babies = [
Baby(name: "彼得潘", age: 1, rabbit: Rabbit(name: "彼得兔")),
Baby(name: "虎克船長", age: 2, rabbit: Rabbit(name: "米菲兔"))
]
print(babies.map(\.rabbit.name))
- filter
let booksAfter1800 = books.filter { $0.year >= 1800 }
filter 的參數必須回傳 Bool,為了傳入 KeyPath,我們在 Book 裡定義 computed property after1800。
struct Book {
var name: String
var character: String
var year: Int
var after1800: Bool {
year >= 1800
}
}
let booksAfter1800 = books.filter(\.after1800)
- compactMap
傳統寫法: 利用 compactMap 搭配 closure 取得有兔子的書的兔子名字。
struct Book {
var name: String
var character: String
var year: Int
var rabbit: String?
}
var books = [
Book(name: "愛麗絲夢遊仙境", character: "愛麗絲", year: 1865, rabbit: "白兔"),
Book(name: "湯姆歷險記", character: "湯姆", year: 1876),
Book(name: "唐‧吉訶德", character: "唐‧吉訶德", year: 1605)
]
let rabbits = books.compactMap { $0.rabbit }
compactMap 的參數如下,而 Book property rabbit 的型別是 String?,符合 compactMap 參數的回傳型別,因此我們傳入 \.rabbit
。
let rabbits = books.compactMap(\.rabbit)