KeyPath — 是 Swift 中对象属性的类型化路径,表示为一等值。与 KVC(键值编码)中的字符串键不同,KeyPath 在编译阶段由编译器检查:编译器知道根对象的类型、属性本身的类型,并能保证路径存在。KeyPath 在 Combine 中用于响应式绑定,在 SwiftUI 中用于双向绑定,在标准库中用于集合的排序和过滤。根据 Apple Developer, 2025,KeyPath 是 Swift 函数式编程的基本构建块。
要点
KeyPath — 是 Swift 标准库中的一种类型,表示对象属性的路径。与通过点号手动访问属性(object.property)或使用字符串键(value(forKey:))不同,KeyPath 是一等对象:它可以用作参数传递、存储在变量中并进行组合。
KeyPath 通过反斜杠和属性路径编写:\Person.name。路径可以嵌套:\Person.address.city。编译器检查 Person 是否有 name 属性,并且 name 确实是 String 类型。如果属性不存在或类型不正确,代码将无法编译。
struct Person: Codable {
let name: String
let age: Int
var isActive: Bool
}
// KeyPath(只读)
let namePath: KeyPath<Person, String> = \Person.name
let person = Person(name: "Alice", age: 30, isActive: true)
let name = person[keyPath: namePath]
通过 KeyPath 访问属性通过下标 object[keyPath: path] 进行。对于只读属性,使用 KeyPath。如果属性是可变的(var),则可以使用 WritableKeyPath。对于引用类型 — ReferenceWritableKeyPath,允许通过 KeyPath 本身更改属性。
KeyPath 具有反映属性访问级别的子类型层次结构。基本层次结构:AnyKeyPath → PartialKeyPath → KeyPath → WritableKeyPath → ReferenceWritableKeyPath。每个子类型都增加了功能。
KeyPath — 只读。WritableKeyPath — 值类型(struct)的读写。ReferenceWritableKeyPath — 引用类型(class)的读写。Swift 根据上下文自动选择合适的子类型。
| 类型 | 访问 | 对象类型 | 修改 |
|---|---|---|---|
| KeyPath | 只读 | 任意 | 否 |
| WritableKeyPath | 读写 | 值类型(inout) | 通过 mutating 上下文 |
| ReferenceWritableKeyPath | 读写 | 引用类型 | 直接赋值 |
// WritableKeyPath 用于值类型
var mutablePerson = Person(name: "Bob", age: 25, isActive: false)
let writablePath: WritableKeyPath<Person, Bool> = \Person.isActive
mutablePerson[keyPath: writablePath] = true
// ReferenceWritableKeyPath 用于类
class User: NSObject {
@objc dynamic var name: String = ""
}
let user = User()
let refPath: ReferenceWritableKeyPath<User, String> = \User.name
user[keyPath: refPath] = "Charlie"
重要区别:对于值类型,通过 KeyPath 设置需要 var(inout 上下文),而对于引用类型 — 需要可变属性。这符合 Swift 的通用语义:值类型按值传递,因此更改需要容器的可变性。
KeyPath 在 Combine 和 SwiftUI 中扮演核心角色。在 Combine 中,assign 运算符使用 KeyPath 将发布者的值绑定到对象属性。在 SwiftUI 中,Binding 使用 KeyPath 进行模型和视图的双向绑定。
assign(to:on:) 运算符接受 ReferenceWritableKeyPath 和一个对象。当发布者发送值时,它会自动写入指定属性。这是一种声明式管理状态的方式,无需手动赋值。
import Combine
class SettingsViewModel: ObservableObject {
@Published var volume: Float = 0.5
var cancellables = Set<AnyCancellable>()
func bindSlider(publisher: AnyPublisher<Float, Never>) {
publisher
.assign(to: \SettingsViewModel.volume, on: self)
.store(in: &cancellables)
}
}
在 SwiftUI 中,@Binding 使用 KeyPath 连接父视图和子视图。父视图传递通过 Binding(get:set:) 或投影($)创建的 Binding
KeyPath 允许编写灵活且类型安全的排序和过滤函数。您不需要每次都传递闭包,而是可以传递指向要排序的属性的 KeyPath。这使得代码更简洁并减少了重复。
sorted
extension Sequence {
func sorted<Value: Comparable>(
by keyPath: KeyPath<Element, Value>,
ascending: Bool = true
) -> [Element] {
ascending
? self.sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
: self.sorted { $0[keyPath: keyPath] > $1[keyPath: keyPath] }
}
}
let sortedByName = people.sorted(by: \Person.name)
let sortedByAgeDesc = people.sorted(by: \Person.age, ascending: false)
这样的实现无需编写闭包即可按任何 Comparable 属性排序。按姓氏排序只需更改 KeyPath:\Person.lastName。如果需要按自定义键排序(例如,名称长度),则在内部使用 map—开发者仍然可以为非标准情况编写自己的闭包。
AnyKeyPath — 是 KeyPath 层次结构中的基类型,它擦除了根和值的具体类型信息。AnyKeyPath 不知道它在什么对象类型上被调用以及返回什么值类型。这对于在集合中存储异构 KeyPath 以及反射非常有用。
当需要存储指向不同类型的不同属性的路径列表时,使用 AnyKeyPath。例如,在 UI 配置器中,每个路径配置特定属性,属性类型可能不同。AnyKeyPath 允许与所有路径进行统一操作。
protocol Configurable {
func apply(_ keyPath: any PartialKeyPath<Self>, value: any)
}
extension Configurable {
func configure(_ pairs: (any KeyPath<Self, any>, any)...) {
for (path, value) in pairs {
apply(path, value: value)
}
}
}
extension Person: Configurable { }
// 允许通过 AnyKeyPath 配置属性
AnyKeyPath 在提取值时需要 类型转换,因为具体类型已被擦除。为了完全的类型安全性,始终首选类型化的 KeyPath
KeyPath 比 KVC(value(forKey:))的字符串键性能显着更高。字符串键通过 Objective-C 运行时机制计算,包括字符串解析、运行时查找和类型转换。KeyPath 是一种基于 Swift 原生结构的静态机制。
根据测试,KeyPath 在属性访问方面平均比 value(forKey:) 快 10-20 倍。差异在于没有运行时查找和动态调度。在编译器充分优化的情况下,KeyPath 直接访问属性在内存中的偏移量。
func readWithKeyPath(person: Person) -> String {
person[keyPath: \Person.name]
}
func readWithKVC(person: Person) -> String {
person.value(forKey: "name") as! String
}
KVC 不仅更慢,而且类型不安全:字符串 “name” 可能不存在,as! String 转换可能会崩溃。KeyPath 在编译阶段保证属性的存在和类型的正确性。对于性能关键部分(动画、数千个元素的列表),KeyPath 是唯一正确的选择。
常见问题
KeyPath — 是指向对象属性的指针,可以像普通值一样传递。您无需编写 person.name,而是创建 \Person.name 路径,并使用它来读取或写入任何 Person 实例中的属性。
KeyPath — 只读。WritableKeyPath — 通过 inout 上下文读写值类型(struct)。ReferenceWritableKeyPath — 无需额外条件即可读写引用类型(class)。
KeyPath 广泛用于 Combine(assign(to:on:))、SwiftUI(Binding, FocusState)、集合排序(sorted(by:))、Core Data(NSSortDescriptor)以及安全访问嵌套属性。
是的,KeyPath 是完全类型安全的。路径中的错误(拼写错误、类型错误)在编译阶段而非运行时被检测到。value(forKey:) 使用字符串,如果键不存在,可能会因 NSUndefinedKeyException 而崩溃。
直接来说 — 不可以,Swift 不支持从字符串动态创建 KeyPath。但是,您可以使用 AnyKeyPath 来存储和传递静态获取的 KeyPath。要通过字符串动态访问属性,请使用带有 Objective-C 运行时的 value(forKey:)。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。