Swift语言中的Key-Path特性浅析

Key-Path字面理解为键路径,熟悉Objective-C语言的同学知道,OC中有一种语法叫做KVC,即简直编码,其作用是允许开发者通过字符串路径来访问对象的属性,这也是Objective-C语言动态化的一种特性。总所周知,Swift是一种强调静态类型与编码安全的语言,因此动态化本身在Swift语言中并不是一种受欢迎的特性,静态化的编程可以让更多的异常问题在编译时被检查出来,并且从开发直觉上来说,静态化也比动态化的代码逻辑更容易理解。

然而,动态化确实也可以代码极大的好处,尤其是一些特殊的应用场景中,动态化可以允许开发者在运行时决定要操作数据,可以使用简洁的代码编写出非常强大的功能逻辑。

回归正题,文本主要讨论Swift编程语言中的Key-Path特性,Key-Path与Objective-C中的KVC有些类似,其为Swift增加了一种动态访问对象属性的能力,但对KVC的完全动态化又进行了一些限制,Swift中定义了一种特殊的类型来描述对象键的路径,这就是Key-Path。那么它有什么用?怎么用呢?这是本文要介绍的重点,希望能给你带来启发。

什么是Key-Path

编程的本质是使用特定的语言来描述算法或数据。Key-Path是Swift中内置的一种类型,定义如下:

class KeyPath<Root, Value>

可以看到这个Key-Path类的定义关联了两个泛型,Root描述的是支持此Key-Path的对象本身的类型,Value描述的是此Key-Path对应的路径的属性的类型。这么讲可能有些抽象,我们通过一个小例子来看就比较清晰了。

// k是一个key-path对象
let k: KeyPath<String, Int> = \String.count

let str = "Hello World"
// 将读取到str的count属性
let value = str[keyPath: k]
// 11
print(value)

Key-Path在定义时,语法是使用\符号,之后跟上对象类型与属性名,属性名是支持链式描述的。

自定义的类型也可以很方便的使用Key-Path,例如:

struct Subject {
    var name: String
}

struct Student {
    var name: String
}

struct Teacher {
    var name: String
    var age: Int
    var subject: Subject
    var students: [Student]
}

let t = Teacher(name: "Teacher Wang", age: 34, subject: .init(name: "Math"), students: [.init(name: "Xiaoming"), .init(name: "Linlin")])
// Teacher Wang
print(t[keyPath: \Teacher.name])
// Math
print(t[keyPath: \Teacher.subject.name])
// Linlin
print(t[keyPath: \Teacher.students[1].name])

可以看到,Key-Path路径除了可以链式定义,还支持数组下标。另外,如果在上下文中是可以推断出Key-Path的Root泛型具体类型时,也可以省略对象的类型部分,例如:

// 34
print(t[keyPath: \.age])

在属性路径中也支持使用self来描述对象本身,例如:

// 当前Teacher对象本身:t
print(t[keyPath: \.self])

如果路径中的某个属性是可选的,那么在定义Key-Path时,也可以类似可选链的方式进行拆包,如下:

class Class {
    var t: Teacher?
}

let c = Class()
// nil
print(c[keyPath: \.t?.name])

Key-Path的一些应用

从语法层面上来看,Key-Path的写法非常简洁,在某些场景下,我们可以用其来代替闭包或函数。最常见的应用场景为filter、map这些方法。例如:

let teachers = [Teacher(name: "A", age: 30, subject: .init(name: "A"), students: []),
                Teacher(name: "B", age: 20, subject: .init(name: "B"), students: []),
                Teacher(name: "C", age: 33, subject: .init(name: "C"), students: [])]
// 会将所有Teacher的name map出来
let names = teachers.map(\.name)
// ["A", "B", "C"]
print(names)