一、前言
- 自從 Swift 剛開始就被設計為是編譯時安全和靜態型別后,它就缺少了那種經常在運行時語言中的動態特性,比如 Object-C, Ruby 和 JavaScript,舉個例子,在 Objective-C 中,我們可以很輕易的動態去獲取一個物件的任意屬性和方法,甚至可以在運行時交換它們的實作,
- 雖然缺乏動態性正是 Swift 如此強大的一個重要原因,它幫助我們撰寫更加可以預測的代碼以及更大的保證了代碼撰寫的準確性, 但是有時候,能夠撰寫具有動態特性的代碼是非常有用的,
- 值得慶幸的是,Swift 不斷獲取越來越多的更具動態性的功能,同時還一直把它的關注點放在代碼的型別安全上,其中的一個特性就是 Key Paths,那么 Key Paths 是如何在 Swift 中作業的呢?它有哪些非常炫酷非常有用的事情值得讓我們去做?
二、Key Paths 基礎
- Key Paths 基本上可以讓我們將任何實體屬性參考為一個單獨的值,因此,它們可以通過運算式傳遞,并使一段代碼能夠獲取或設定一個屬性而無需實際了解該屬性,
- Key Paths 有三種主要變種:
-
- KeyPath:提供對屬性的只讀訪問權限;
-
- WritableKeyPath:提供對具有值語意的可變屬性的讀寫訪問權限(因此所討論的實體也需要是可變的,以便允許的寫入);
-
- ReferenceWritableKeyPath: 只能與參考型別(例如類的實體)一起使用,并為任何可變屬性提供讀寫訪問權限,
- 還有一些額外的 Key Paths 型別,即可以減少內部代碼復制并幫助型別擦除,但我們將專注于本文中的主要型別,我們來深入查看如何使用 Key Paths,以及是什么讓它們變得有趣并具有潛在的強大功能,
三、功能演示
- 設我們正在構建一個應用程式,它可以讓用戶從 Web 閱讀文章,并且有一個用來代表一個這樣的文章的 Article 模型,看起來像這樣:
struct Article {
let id: UUID
let source: URL
let title: String
let body: String
}
- 每當我們使用這樣的模型的陣列時,從每個模型中提取一段資料來形成一個新的陣列是很常見的,如下所示兩個例子,從一個文章陣列中收集所有的 id 和來源:
let articleIDs = articles.map { $0.id }
let articleSources = articles.map { $0.source }
- 雖然上面的方法完全有效,但由于我們只對從每個元素中提取單個值感興趣,因此實際上并不需要閉包的全部功能,因此使用 Key Paths 可能是非常合適的,
- 我們將從擴展 Sequence 開始來添加一個覆寫 map,它采用一個 Key Paths 而不是一個閉包,由于我們只對這個用例的只讀訪問感興趣,所以我們將使用標準的 KeyPath 型別,為了實際執行資料提取,將使用給定的鍵路徑作為引數下標,如下所示:
extension Sequence {
func map<T>(_ keyPath: KeyPath<Element, T>) -> [T] {
return map { $0[keyPath: keyPath] }
}
}
- 如果使用的 Swift 5.2 或更高版本,則不再需要上述擴展,因為現在可以將 Key Paths 自動轉換為函式,
- 通過以上擴展,現在能夠使用一個非常好的和簡單的語法來從任何序列中的每個元素中提取單個值,使得可以從之前轉換我們的示例:
let articleIDs = articles.map(\.id)
let articleSources = articles.map(\.source)
- 這是非常炫酷的,但是,當 Key Paths 真正開始發光時,它們用于形成稍微復雜的運算式,例如在排序一系列值時,標準庫能夠自動對包含 Sortable 元素的任何序列進行排序,但對于所有其它型別,我們必須提供自己的排序閉包,但是,使用 Key Paths,可以通過基于 Comparable 的 Key Paths 輕松添加用于對任何序列進行排序的支持,就像之前一樣,我們將在序列 Sequence 協議中添加一個擴展,將給定 Key Paths 轉換為排序運算式閉包:
extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted { a, b in
return a[keyPath: keyPath] < b[keyPath: keyPath]
}
}
}
- 使用上面的擴展,我們現在能夠快速而簡單地對任何序列進行排序,只需給出想要排序的 Key Paths ,如果我們正在構建的應用程式處理任何形式的可排序串列,例如包含播放串列的音樂應用程式,這非常方便,因為我們現在自由地對串列進行排序,甚至是嵌套:
playlist.songs.sorted(by: \.name)
playlist.songs.sorted(by: \.dateAdded)
playlist.songs.sorted(by: \.ratings.worldWide)
- 這樣做的似乎只是簡單地添加了一個語法糖,但可以制作一些更復雜的代碼處理的序列同時更容易閱讀,并且還可以幫助減少代碼復制,因為現在還能夠為任何屬性重用相同的排序代碼,
四、不需要實體
- 雖然適量的語法很好,但是 Key Paths 的真正威力來自于,它可以讓我們參考屬性而不必與任意的實體相關聯,延續使用之前的音樂主題,假設我們正在開發一個展示歌曲串列的 App,并且在 UI 中為這個串列配置 UITableViewCell,使用如下的配置型別:
struct SongCellConfigurator {
func configure(_ cell: UITableViewCell, for song: Song) {
cell.textLabel?.text = song.name
cell.detailTextLabel?.text = song.artistName
cell.imageView?.image = song.albumArtwork
}
}
- 再次宣告,上面的代碼沒有一點問題,但是我們期望以這樣的方式渲染其她的模型的概率非常的高(非常多的 tableView 的 cells 嘗試著去渲染標題,副標題以及圖片而不用去管它們代表的是什么模型),因此讓我們看看,我們能否用 Key Paths 的威力去創建一個共享的配置實作,讓他可以被任意的模型使用,
- 創建一個名叫 CellConfigurator 的泛型,然后因為我們想要用不同的模型去渲染不同的資料,所以將會給它提供一組基于 Key Paths 的屬性,先渲染其中的一個資料:
struct CellConfigurator<Model> {
let titleKeyPath: KeyPath<Model, String>
let subtitleKeyPath: KeyPath<Model, String>
let imageKeyPath: KeyPath<Model, UIImage?>
func configure(_ cell: UITableViewCell, for model: Model) {
cell.textLabel?.text = model[keyPath: titleKeyPath]
cell.detailTextLabel?.text = model[keyPath: subtitleKeyPath]
cell.imageView?.image = model[keyPath: imageKeyPath]
}
}
- 上面的實作優雅的地方在于,我們現在可以為每個模型定制 CellConfigurator,使用相同的輕量的 Key Paths 語法,如下所示:
let songCellConfigurator = CellConfigurator<Song>(
titleKeyPath: \.name,
subtitleKeyPath: \.artistName,
imageKeyPath: \.albumArtwork
)
let playlistCellConfigurator = CellConfigurator<Playlist>(
titleKeyPath: \.title,
subtitleKeyPath: \.authorName,
imageKeyPath: \.artwork
)
- 就像標準庫中的 map 和 sorted 等函式的操作一樣,曾經可能會使用閉包去實作 CellConfigurator,然而,通過 Key Paths 能夠使用一個非常好的語法去實作它,并且也不需要任何的訂制化的操作去不得不通過模型實體去處理,使它們變得更加的簡單,更加的具有說服力,
五、轉化為函式
- 目前為止,僅僅使用 Key Paths 來讀取值,那么如何使用它們來動態的寫值呢?在很多不同的代碼中,我們常常可以見到一些像下面的代碼一樣的列子,我們通過這段代碼來加載一系列的事項,然后在 ListViewController 中去渲染它們,然后當加載操作完成后,我們會簡單的將加載的事項賦值給視圖控制器中的屬性:
class ListViewController {
private var items = [Item]() { didSet { render() } }
func loadItems() {
loader.load { [weak self] items in
self?.items = items
}
}
}
- 我們看看通過 Key Paths 賦值能否讓上面的語法簡單一點,并且能夠移除我們經常使用的 weak self 的語法(如果忘記對 self 的參考前加上 weak 關鍵字的話,那么就會產生回圈參考),既然所有上面我們做的事情都是獲取傳遞給我們閉包的值,并將它賦值給視圖控制器中的屬性,那么如果我們真的能夠將屬性的 setter 作為函式傳遞,會不會很酷呢?這樣的話,就可以直接將函式作為完成閉包傳遞給我們的加載方法,然后所有的事情都會正常執行,
- 為了實作這一目標,首先定義一個函式,讓任意的可寫的轉化為一個閉包,然后為 Key Paths 設定屬性值,為此,我們將會使用 ReferenceWritableKeyPath 型別,因為只想把它限制為參考型別(否則只會改變本地屬性的值),給定一個物件,以及給這個物件設定 Key Paths,我們將會自動將捕獲的物件作為弱參考型別,一旦函式被呼叫,就會給匹配 Key Paths 的屬性賦值,如下所示:
func setter<Object: AnyObject, Value>(
for object: Object,
keyPath: ReferenceWritableKeyPath<Object, Value>
) -> (Value) -> Void {
return { [weak object] value in
object?[keyPath: keyPath] = value
}
}
- 使用上面的代碼,可以簡化之前的代碼,將弱參考的 self 去除,然后用看起來非常簡潔的語法結尾:
class ListViewController {
private var items = [Item]() { didSet { render() } }
func loadItems() {
loader.load(then: setter(for: self, keyPath: \.items))
}
}
- 到這里,非常酷有沒有?或許它還能變得更加的酷,當上面的代碼跟更加先進的函式式編程思想結合在一起的時候,如組合函式,因此我們現在可以將多個 setter 函式和其它的函式鏈接在一起使用,
六、使用 Key Paths 創建方便的 API
- Swift 的 Key Paths 可以構建非常強大的 API,而且在呼叫站點上看起來非常漂亮和干凈,這里我們有一個擴展,讓我們可以輕松組任何基于一個 key path 的 Sequence:
// This extension will let us group any Sequence (such as an
// Array or a Set), based on a given key path.
extension Sequence {
func grouped<T: Hashable>(by keyPath: KeyPath<Element, T>) -> [T: [Element]] {
// Using key path subscripting syntax, we can dynamically
// access a member of a type based on a key path.
return .init(grouping: self, by: { $0[keyPath: keyPath] })
}
}
func scan(_ string: String, using matchers: [Matcher]) {
// The result is that our call sites become really clean, since
// we can simply use a key path literal to group any sequence.
let matchersByPattern = (
start: matchers.grouped(by: \.pattern.start),
end: matchers.grouped(by: \.pattern.end)
)
...
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/336272.html
標籤:其他
