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)

參考資料

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com