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关键路径这样的功能有点困难,而且很容易将它们看作是简单的语法糖。可以使用更加动态的方法去引用属性是一件很是强大的事情,即便闭包一般能够作不少相似的事情,可是轻量的语法以及关键路径的声明,都使他们可以成为处理很是多种类的数据的好的匹配。

谢谢阅读!

相关文章
相关标签/搜索