一、前言

  • 自从 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: UUIDlet source: URLlet title: Stringlet 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 inreturn 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.namecell.detailTextLabel?.text = song.artistNamecell.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 inself?.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 inobject?[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))...
}

Swift之深入解析Key Paths的功能与应用相关推荐

  1. Swift之深入解析可选链的功能和使用

    一.什么是可选链? 可选链(Optional Chaining)是一种可以请求和调用属性.方法和子脚本的过程,用于请求或调用的目标可能为nil. 可选链返回两个值: 如果目标有值,调用就会成功,返回该 ...

  2. Swift之使用key paths创建自定义查询函数 | CSDN创作打卡

    一.前言 作为一个相当严格,静态编译的语言,Swift 可能不会在语法自定义方面提供许多渠道,但这实际上确正好相反.通过如何在 Swift 中自定义操作符,Swift 中 key paths 的能力, ...

  3. Swift之深入解析如何使用Xcode和LLDB v2修改UI元素

    一.前言 在上一篇博客中,已经详细地介绍如何使用 LLDB 表达式修改 UI 元素,具体请参考:Swift之深入解析如何将代码添加为自定义LLDB命令. 在这篇博客中,将继续讨论相同的问题需求,并将重 ...

  4. Swift之深入解析如何避免单元测试中的强制解析

    一.前言 强制解析(使用 !)是 Swift 语言中不可或缺的一个重要特点(特别是和 Objective-C 的接口混合使用时),它回避了一些其他问题,使得 Swift 语言变得更加优秀. 比如在我的 ...

  5. Swift 类似HandyJSON解析Struct

    Swift 类似HandyJSON解析Struct HandyJSON 从源码解析Struct 获取TargetStructMetadata 获取TargetStructDescriptor 实现Ta ...

  6. vSAN架构解析与6.7功能介绍

    vSAN架构解析与6.7功能介绍 2018-11-16 09:43:40 来源: IT大咖说举报 2 分享至 阅读字数:6759 | 17分钟阅读 Overview of vSAN About vSA ...

  7. 通过调试微信小程序示例代码解析flex布局参数功能(一)

    通过调试微信小程序示例代码解析flex布局参数功能 官方示例小程序源代码下载地址:https://github.com/wechat-miniprogram/miniprogram-demo 通过调试 ...

  8. Swift实现iOS录音与播放音频功能

    Swift实现iOS录音与播放音频功能 作用 AVPLayer:可以用来播放在线及本地音视频 AVAudioSession:音频会话,主要用来管理音频设置与硬件交互 使用时需要导入 #import & ...

  9. swift php json解析,Swift 4.0 | JSON数据的解析和编码

    文 / 菲拉兔 自己撸的图 要求: Platform: iOS8.0+ Language: Swift4.0 Editor: Xcode9 [问题补充2017-09-28] 最近我发现了一个问题:在S ...

最新文章

  1. Android logcat命令详解
  2. 测试无线节能信标与主控器之间的控制功能
  3. 【五线谱】符干朝向与连音线 ( 符干朝向 | 第三线以下符干朝上 | 第三线以上符干朝下 | 连音线 )
  4. 2019秋第三周学习总结
  5. 特征检测和跟踪经典理论
  6. Java线程的概念:什么是线程?
  7. JavaScript push(),join() 函数
  8. scala怎么做幂运算_Scala幂(幂)函数示例
  9. C++ 使用move来删除用户指定的文件
  10. html5 自制播放器
  11. Git Branch Mode(分支模式)
  12. metasploit下载教程linux,在Debian 10/9系统上安装Metasploit Framework的方法
  13. 第六周--------职业规划------正确的选择让你少奋斗十年
  14. 支付宝新漏洞引发恐慌,那如何关闭小额免密支付呢
  15. H5 +蓝牙打印机 CPCL和ESC
  16. 多因子模型与细分行业多因子测试源码(以医疗行业为例)
  17. C# 开发ModBus的服务器程序 实现ModBus数据总站 搭建自定义的Modbus服务器 同时支持tcp和rtu...
  18. HFSS学习笔记—8.Optimetrics模块的使用
  19. 四海八荒齐聚,200家企业引爆温州乌市 “数据浪潮”
  20. 自动控制原理基础——拉普拉斯变换

热门文章

  1. Kubernetes容器集群 - harbor仓库高可用集群部署说明
  2. python之闭包,装饰器
  3. hihoCoder #1872 : Pythagorean triple
  4. List的remove(对象)操作有时候会报ConcurrentModificationException异常
  5. CentOS7——卡在在启动界面
  6. 昨天登陆页面,无法进入后台,今天攻克了
  7. Android官方文章翻译之管理设备苏醒状态(Managing Device Awake State)(二)
  8. SoC嵌入式软件架构设计之二:虚拟内存管理原理、MMU硬件设计及代码分块管理...
  9. M2 Scrum 12.05
  10. 深有体会的积极人生态度