KeyPath在Swift中的妙用

原文連接: The power of key paths in Swift編程

自從swift剛開始就被設計爲是編譯時安全和靜態類型後,它就缺乏了那種我麼常常在運行時語言中的動態特性,好比Object-C, Ruby和JavaScript。舉個例子,在Object-C中,咱們能夠很輕易的動態去獲取一個對象的任意屬性和方法 - 甚至能夠在運行時交換他們的實現。swift

雖然缺少動態性正是Swift如此強大的一個重要緣由 - 它幫助咱們編寫更加能夠預測的代碼以及更大的保證了代碼編寫的準確性, 可是有的時候,可以編寫具備動態特性的代碼是很是有用的。數組

值得慶幸的是,Swift不斷獲取愈來愈多的更具動態性的功能,同時還一直把它的關注點放在代碼的類型安全上。其中的一個特性就是KeyPath。這周,就讓咱們來看看KeyPath是如何在Swift中工做的,而且有哪些很是酷很是有用的事情可讓咱們去作。安全

基礎知識

Key paths實質上可讓咱們將任意一個屬性當作一個獨立的值。所以,它們能夠傳遞,能夠在表達式中使用,啓用一段代碼去獲取或者設置一個屬性,而不用確切地知道它們使用哪一個屬性。網絡

Key paths主要有三種變體:閉包

  • KeyPath: 提供對屬性的只讀訪問
  • WritableKeyPath: 提供對具備價值語義的可變屬性提供可讀可寫的訪問
  • ReferenceWritableKeyPath: 只能對引用類型使用(好比一個類的實例), 對任意可變屬性提供可讀可寫的訪問

這裏有一些額外的keypath類型,它們能夠減小內部重複的代碼,以及能夠幫助咱們作類型的擦除,可是咱們在這篇文章中,會專一於上面的三種主要的類型。app

讓咱們深刻了解如何使用key paths吧,以及使它們變得有趣且很是強大的緣由。ide

功能速記

咱們這樣說吧,咱們正在構建一個可讓咱們閱讀從網絡上獲取到文章的app,以及咱們已經有一個Article的模型用來表達這篇文章,就像下面這樣:函數式編程

struct Article {
    let id: UUID
    let source: URL
    let title: String
    let body: String
}
複製代碼

不管何時,咱們使用這個模型數組,一般須要從每一個模型中提取單個數據以組成新的數組 - 就像下面這兩個從文章數組中獲取全部的IDs和sources的列子同樣:函數

let articleIDs = articles.map { $0.id }
let articleSources = articles.map { $0.source }
複製代碼

雖然上面的實現徹底沒有問題,可是咱們只是想要從每一個元素中提取單一的值,咱們不是真的須要閉包的全部功能 - 因此使用keypath就可能很是合適。讓咱們看看它是如何工做的吧。

咱們會經過在Sequence協議中重寫map方法來處理key path,而不是經過閉包。既然咱們只對這個使用例子的只讀訪問有興趣,那麼咱們將會使用標準的KeyPath類型,而且爲了實際的數據提取,咱們將會使用給定的鍵值路徑做爲下標參數,以下所示:

extension Sequence {
	func map<T>(_ keyPath: KeyPath<Element, T>) -> [T] {
	  return map { $0[keyPath: keyPath] }
	}
}
複製代碼

隨着上述準備就緒,咱們可以使用友好和簡單的語法來從任意的序列元素中提取出單一的值,使將以前的列子轉化成下面的樣子成爲可能:

let articleIDs = articles.map(\.id)
let articleSources = articles.map(\.source)
複製代碼

這是很是酷的,可是鍵值路徑真正開始閃光的時候,是當它們被用來組成更加靈活的表達式 - 好比當給序列的值排序的時候。

標準庫能夠給任意的包含可排序的元素的序列進行自動排序,可是對於其餘不可排序的元素,咱們必須提供本身的排序閉包。然而,使用關鍵路徑,咱們能夠很簡單的給任意的可比較的元素添加排序的支持。就像以前同樣,咱們給序列添加一個擴展,來將給定的關鍵路徑在排序表達閉包中進行轉化:

extension Sequence {
    func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
        return sorted { a, b in
            return a[keyPath: keyPath] < b[keyPath: keyPath]
        }
    }
}
複製代碼

使用上述方法,咱們能夠很是快速的排序任意的序列,只用簡單的提供一個咱們指望被排序的關鍵路徑。若是咱們構建的app是用來處理任意的可排序的列表 - 舉個例子,一個包含了播放列表的音樂app - 這將是很是有用的,咱們如今能夠隨意排序基於可比較屬性的列表(甚至是嵌套的屬性)。

playlist.songs.sorted(by: \.name)
playlist.songs.sorted(by: \.dateAdded)
playlist.songs.sorted(by: \.ratings.worldWide)
複製代碼

完成上述的事情看起來有點簡單,就像添加了一個語法糖。可是既能夠寫出更加靈活的代碼去處理序列,讓他們更易讀,也能夠減小重複的代碼。所以咱們如今可以爲任意屬性重用相同的排序代碼。

不須要實例

雖然適量的語法糖很好,可是關鍵路徑的真正的威力來自於,它可讓咱們引用屬性而沒必要與任意的實例相關聯。延續使用以前的音樂主題,假設咱們正在開發一個展現歌曲列表的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嘗試着去渲染標題,副標題以及圖片而不用去管他們表明的是什麼模型)- 所以讓咱們看看,咱們可否用關鍵路徑的威力去建立一個共享的配置實現,讓他能夠被任意的模型使用。

讓咱們建立一個名叫CellConfigurator的泛型,而後由於咱們想要用不一樣的模型去渲染不一樣的數據,因此咱們將會給它提供一組基於關鍵路徑的屬性 - 咱們先渲染其中的一個數據:

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,使用相同的輕量的關鍵路徑語法,以下所示:

let songCellConfigurator = CellConfigurator<Song>(
    titleKeyPath: \.name,
    subtitleKeyPath: \.artistName,
    imageKeyPath: \.albumArtwork
)

let playlistCellConfigurator = CellConfigurator<Playlist>(
    titleKeyPath: \.title,
    subtitleKeyPath: \.authorName,
    imageKeyPath: \.artwork
)
複製代碼

就像標準庫中的map和sorted等函數的操做同樣,咱們曾經可能會使用閉包去實現CellConfigurator。然而,經過關鍵路徑,咱們可以使用一個很是好的語法去實現它 - 而且咱們也不須要任何的訂製化的操做去不得不經過模型實例去處理 - 使它們變得更加的簡單,更加的具備說服力。

轉化爲函數

目前爲止,咱們僅僅使用關鍵路徑來讀取值 - 如今讓咱們看看咱們如何使用它們來動態的寫值。在不少不一樣的代碼中,咱們經常能夠見到一些像下面的代碼同樣的列子 - 咱們經過這段代碼來加載一系列的事項,而後在ListViewController中去渲染它們,而後當加載操做完成後,咱們會簡單的將加載的事項賦值給視圖控制器中的屬性。

class ListViewController {
    private var items = [Item]() { didSet { render() } }

    func loadItems() {
        loader.load { [weak self] items in
            self?.items = items
        }
    }
}
複製代碼

讓咱們看看,經過關鍵路徑賦值可否讓上面的語法簡單一點,而且可以移除咱們常用的weak self的語法(若是咱們忘記對self的引用前加上weak關鍵字的話,那麼就會產生循環引用)。

既然全部上面咱們作的事情都是獲取傳遞給咱們閉包的值,並將它賦值給視圖控制器中的屬性 - 那麼若是咱們真的可以將屬性的setter做爲函數傳遞,會不會很酷呢?這樣咱們就能夠直接將函數做爲完成閉包傳遞給咱們的加載方法,而後全部的事情都會正常執行。

爲了實現這一目標,首先咱們先定義一個函數,讓任意的可寫的轉化爲一個閉包,而後爲關鍵路徑設置屬性值。爲此,咱們將會使用ReferenceWritableKeyPath類型,由於咱們只想把它限制爲引用類型(不然的話,咱們只會改變本地屬性的值)。給定一個對象,以及給這個對象設置關鍵路徑,咱們將會自動將捕獲的對象做爲弱引用類型,一旦咱們的函數被調用,咱們就會給匹配關鍵路徑的屬性賦值。就像這樣:

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函數和其餘的函數連接在一塊兒使用。在接下來的文章中,咱們將介紹函數式編程和組合函數。

總結

首先,看起來如何以及什麼時候去使用swift關鍵路徑這樣的功能有點困難,而且很容易將它們看作是簡單的語法糖。可以使用更加動態的方法去引用屬性是一件很是強大的事情,即便閉包一般能夠作不少相似的事情,可是輕量的語法以及關鍵路徑的聲明,都使他們可以成爲處理很是多種類的數據的好的匹配。

謝謝閱讀!

相關文章
相關標籤/搜索