Swift KeyPath教程(翻译)

译自 The power of key path in Swift

Swift 最初的设计就专注于编译期尽量保证程序运行安全 以及 静态类型匹配,因此它比较缺乏类似 OC,Javascript 和ruby等运行时语言的动态特性。比如,在OC里,我们可以在运行时动态访问类对象的属性和方法,甚至改变方法的实现。

但这些动态特性的缺乏正表现出Swift的伟大,它能保证我们写出的程序运行结果更加可预料,且更可能运行不出错。不过有时候我们的代码能拥有一些动态特性也是很有帮助的。

谢天谢地,随着版本更新,Swift 开始拥有更多的动态特性,但却依然保持着静态安全类型特性。其中一种新特性就是KeyPath。本周,我们一起来看看KeyPath在Swift中是如何运作的,以及我们能用它来实现哪些强大的功能。

基础

KeyPath 本质上让我们可以用一个值来引用任意对象的特定属性。像这样,它可以被到处传递引用,在表达式中使用,甚至可以在不用知道它是哪个类的属性时,对它进行set 和get 操作。

KeyPath一般有三种类型:

  • KeyPath:提供只读的属性访问
  • WritableKeyPath: 通过特定语法,提供某个对象的可变更属性的读写权限。(该对象必须是可修改的)
  • ReferenceWritableKeyPath: 只能对可引用的类型使用,比如类的对象实例。同上,提供该对象的某个可修改的属性的读写权限。

还有一些其他的keyPath类型,用来减少代码重复性,同时移除类型限制。不过我们暂时只关注主要的这几个类型。

让我们来看看如何使用KeyPath,以及为什么它这么有趣又功能强大。

功能速记

比如说我们要开发一个 让用户可以从各种Web上阅读文章的App,可以设计一个Article 数据模型来表示一篇文章,像这样:

struct Article {
    let id: UUID
    let source: URL
    let title: String
    let body: String
}

当我们使用这种数据模型的数组时,经常需要从类里摘取单一属性出来重新生成一个新的数据数组。比如下面的例子,我们将所有id 和source数据拿出来生成新的数据数组。

let articleIDs = articles.map { $0.id }
let articleSources = articles.map { $0.source }

当我们只需要使用到一个属性数据时,上面的代码是ok的。但其实我们不需要使用到闭包的所有功能,这时候使用keypath 就是比较合适的选择了。

我们可以重载Sequence 的map 方法,让它使用一个keypath来替代之前的闭包作为参数。在这个例子里,我们只对只读属性数据感兴趣,因此使用KeyPath 类型即可。使用传入的KeyPath 来生成数据返回,就像下面这样:

extension Sequence {
    func map(_ keyPath: KeyPath) -> [T] {
        return map { $0[keyPath: keyPath] }
    }
}

注意:Swift 5.2 以上的Sequence map函数已经支持用KeyPath作为参数了。automatically converted into functions.

使用上面的扩展方法,我们可以很方便地将对象的单一任意属性从数组里截取出来,上面的例子代码可以修改为:

let articleIDs = articles.map(\.id)
let articleSources = articles.map(\.source)

这很cool,但keypath真正的强大之处我们可以用更复杂一点的表达式体现出来,比如将数组进行排序。

标准库已经可以将任意带有Sortable 元素的序列进行排序了,但是对于所有类型,我们都必须实现自己的排序闭包。然而,使用keypath,我们就可以很简单地使用数组元素的Comparable类型keypath来进行排序了。像上面做的那样,我们给Sequence新增一个扩展方法,将传入的keypath 转换成排序闭包返回。

extension Sequence {
    func sorted(by keyPath: KeyPath) -> [Element] {
        return sorted { a, b in
            return a[keyPath: keyPath] < b[keyPath: keyPath]
        }
    }
}

使用上面的方法,我们可以用指定的keypath来更快速和方便地对一个序列进行排序了。如果我们要开发一个 使用可排序数据列表的app,比如一个带播放列表的音乐app,我们就可以使用任何可比较的属性来对播放列表进行排序,甚至可以使用嵌套的多层属性。

playlist.songs.sorted(by: \.name)
playlist.songs.sorted(by: \.dateAdded)
playlist.songs.sorted(by: \.ratings.worldWide)

像上面那样的代码就像新增了一个语法糖,对任意属性都能使用同样的排序代码,不但能让处理序列的复杂代码的可读性变得更高,也能减少重复代码。

不需要对象实例了

适量的语法糖还是很棒的。keypath的真正强大之处在于我们可以引用任意属性却不用关心它所属的对象实例。回到上面所说的音乐主题,比如说我们开发的app要显示一个音乐列表,需要为一首音乐配置一个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
    }
}

你可以从下面的网站了解到更多关于为视图定义配置信息的知识
"Preventing views from being model aware in Swift".

上面代码没什么问题,但有很高的几率我们还要用同样的风格来渲染数据模型(tableview经常要显示包含标题,副标题以及图片,无论它使用的是什么类型的数据模型)我们来看看是否可以用keypath来创建一套通用配置,无论什么类型的数据模型都能使用。

先创建一个叫 CellConfigurator 的泛型类型,当我们需要为不同的数据模型渲染不同的数据时,可以定义一组基于keypath类型的属性,每一个属性对应一个需要渲染的数据。

struct CellConfigurator {
    let titleKeyPath: KeyPath
    let subtitleKeyPath: KeyPath
    let imageKeyPath: KeyPath

    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,只要使用keypath就可以达到目的。就像这样:

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

let playlistCellConfigurator = CellConfigurator(
    titleKeyPath: \.title,
    subtitleKeyPath: \.authorName,
    imageKeyPath: \.artwork
)

我们可以用闭包来创建CellConfigurator,就像基础库里的功能性函数 sort 和 map方法一样。我们可以使用keypath来实现优美的语法,我们也不用编写特定的代码来与数据模型实例直接打交道了,也让这些配置声明更加简单和易懂。

转换为函数

到目前为止,我们只用了keypath来读取数据,现在来看看我们怎么使用keypath来动态写入数据。在各种开源代码里最常见的就像下面的例子 - 我们在ListViewController里加载渲染一个物品列表,当加载工作完成时,我们将相关数据设置给ViewController的一个属性。

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

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

我们来看看keypath是否可以让上面的语法更加简单,我们是否可以将 weak self这种经常要手动实现的东西给移除掉(使用weak self 主要是为了避免循环引用)。

我们需要做的是将传给我们实现的闭包的数据设置给ViewController的一个属性。如果我们可以为这个属性传递一个setter函数,那是不是就更酷了?方法是我们load函数直接传入一个函数作为回调处理,这样就行的通了。

要实现这个功能,我们需要定义一个可以将传入的可写keypath转换成 为这个keypath设定数据的闭包的函数。这次我们可以使用 ReferenceWritableKeyPath 类型,因为我们需要限定只对可引用类型来使用(不然的话我们改变的是实例数据的拷贝而不是实例本身)。传入对象实例和实例的属性keypath后,就可以自动将实例捕获为weak引用,并在调用返回的闭包后更改keypath对应的属性 - 就像这样:

func setter(
    for object: Object,
    keyPath: ReferenceWritableKeyPath
) -> (Value) -> Void {
    return { [weak object] value in
        object?[keyPath: keyPath] = value
    }
}

用上面的方法,我们就可以将之前的代码简化,去掉weak self的语法,最终得到看上去更干净的代码。

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

    func loadItems() {
        loader.load(then: setter(for: self, keyPath: \.items))
    }
}

太酷了!如果我们将它应用在进阶的功能里,比如函数组合,那就更酷了,比如我们可以将多个setter和其他函数绑定在一起。接下来我们会更深入地探讨keypath功能,函数组合将是接下来的文章话题。

结论

一开始,可能有点难以理解如何以及何时使用swift的keypath,认为它只是简单的语法糖而已 而忽略它的真正强大之处。能更动态地引用对象的属性是一个强大的功能,尽管闭包能经常能达到相似的功能,但简单的语法和可声明性的本质让它成为处理各种各样的数据的很棒的选择。

作为一个比较新出的功能(Swift 4 以上),我相信随着时间的推移,技术社区里会出现关于keypath的新用法和思路,因此在以后的技术文章里我们还会回顾keypath功能。到那时,我也想听听大家关于keypath的想法 - 比如你们是否真的在使用它 或是 正在尝试着使用它?

谢谢阅读我的文章。

你可能感兴趣的:(Swift KeyPath教程(翻译))