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)