关于Swift的KeyPath,你了解多少

513 阅读3分钟

概述

众所周知,Swift是一种静态类型的编程语言,这意味着在编译时会对变量和方法的类型进行检查,以确保程序的类型安全。而在Object-C中,我们可以很轻易的动态去获取一个对象的任意属性和方法 - 甚至可以在运行时交换他们的实现。

值得庆幸的是,Swift在某些方面还是具有一定的动态性。在其引入一些动态性特性的同时,也保持着编译时的类型安全。这些特性如KeyPath、反射等,允许开发人员在运行时动态地查询、修改对象的属性和行为,同时又不会破坏编译时类型检查的机制,确保程序的类型安全性。这既保证了程序的灵活性和可扩展性,又避免了许多运行时类型错误的发生,使Swift成为一种非常受欢迎的编程语言。

接下来,让我们一起看看其中的一个特性KeyPath是如何在Swift中工作的,并且有哪些非常酷非常有用的事情可以让我们去做

定义

KeyPath<T, Property>是Swift语言中的一个非常有用的特性,它允许我们使用一种类型安全的方式来表示和操作对象的属性。使用KeyPath,我们可以在运行时动态地获取和设置对象的属性,而不需要使用动态类型或字符串。

KeyPath使用反射和泛型实现,它通过Type-safe的方式来获取属性的引用。可以考虑将KeyPath看作是属性的访问器,允许您引用对象属性而不需要引用整个对象。

作用

  • 可以在编译时检查:由于Swift是一种静态类型的编程语言,KeyPath也是类型安全的。这意味着在编译时,您可以检查KeyPath是否正确地引用了对象的属性。这有助于避免在运行时出现类型错误
  • 可以用于查询:KeyPath不仅允许您获取属性的引用,还允许您执行查询操作。例如,您可以使用KeyPath来过滤数组中的元素,并只选择满足特定条件的元素
  • 可以用于排序:您可以使用KeyPath来指定排序条件,以便将对象按照指定属性的值进行排序。这是非常有用的,特别是在处理数据集合时
  • 可以组成键路径:您可以使用KeyPath将多个属性组合成一个对象。这为表单、配置文件和其他需要聚合多个属性的操作提供了方便

典型用法

查询

项目开发过程中,我们经常会有这个需求,就是从每个模型中提取单个数据以组成新的数组 - 就像下面这两个从用户数组中获取所有用户的名字或年龄的例子一样:

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

struct User {
    var name: String
    var age: Int
}

let users = [
    User(name: "Alice", age: 25),
    User(name: "Bob", age: 30),
    User(name: "Charlie", age: 35)
]

let user = User(name: "Tom", age: 18)
let keyPath = \User.name
// 通过`KeyPath`获取了实例的 name 属性值
let name = user[keyPath: keyPath]
print(name) // 输出 "Tom"

// 第一种方式: 通过闭包从每个元素中提取属性
let names = users.map { $0.id } 
let ages = users.map { $0.source }

// 第二种方式: 通过KeyPath可以直接从每个元素中提取属性
let names = users.map(\.name)

print(names) // ["Alice", "Bob", "Charlie"]

在性能上,使用 .name 或 .age 作为 KeyPath 参数映射属性,相对于使用闭包从每个元素中提取属性,更加高效。对于一个非常大的序列,这个区别可能会更加明显

在第一种情况下,使用闭包从每个元素中提取属性,并将其存储到一个新的数组中,此时闭包的执行时间比较长。而在第二种情况下,使用 KeyPath 可以直接从每个元素中提取属性,这比使用闭包更快速和高效

因此,建议在获取属性列表时,尽可能使用 KeyPath,避免使用闭包

自动排序

我们都知道,Swift标准库可以给任意的包含可排序的元素的序列进行自动排序,但是对于其他不可排序的元素,我们必须提供自己的排序闭包。然而,使用关键路径,我们可以很简单的给任意的可比较的元素添加排序的支持。就像之前一样,我们给序列添加一个扩展,来将给定的关键路径在排序表达闭包中进行转化

extension Sequence {
    func sorted<T: Comparable>(by keypath: KeyPath<Element, T>) -> [Element] {
        return sorted {
            $0[keyPath: keypath] < $1[keyPath: keypath]
        }
    }
}

/* 
 输出  [
        User(name: "Alice", age: 25), 
        User(name: "Bob", age: 30), 
        User(name: "Charlie", age: 35)
       ]
*/

// 第一种方式
let sortedUsers = users.sorted { $0.age < $1.age }

// 第二种方式
let sortedUser = users.sorted(by: \.age) 
print(sortedUser)

在性能上,使用 users.sorted(by: .age) 形式的 KeyPath 方法通常比使用闭包形式的 sorted(by:) 方法效率更高。这是因为 KeyPath 利用了编译器生成的额外信息,允许在代码执行期间进行更多的优化。

闭包形式的 sorted(by:) 方法需要通过闭包从每个元素中提取出要比较的属性,并在比较时进行比较。而 KeyPath 形式的 sorted(by:) 方法允许直接提取出属性值并进行比较,这更加高效。

因此,建议使用 sorted(by:) 方法与 KeyPath 来对一个序列进行排序,而不是使用闭包的 sorted(by:) 方法

泛型编程

后续更新