一、核心知识点罗列
(一)属性的核心分类与基础特性
-
存储属性(Stored Property)
- 本质:直接存储实际值,占用实例内存,分为变量存储属性(
var)和常量存储属性(let)。 - 访问控制:支持
private(set)/fileprivate(set)等修饰符,实现“外部只读、内部可写”(如private(set) var record: [(CLLocation, Date)] = [])。 - 适用类型:类、结构体(枚举无存储属性,因枚举无实例存储),不能定义在扩展中。
- 自动合成:结构体默认生成成员初始化器(Memberwise Initializer),自动初始化所有存储属性。
- 本质:直接存储实际值,占用实例内存,分为变量存储属性(
-
计算属性(Computed Property)
- 本质:无独立存储空间,本质是“无参数方法”,通过
get(必需)和set(可选)方法实现值的获取与修改。 - 只读计算属性:省略
set方法或直接写var prop: Type { 计算逻辑 }(如var timestamps: [Date] { record.map { $0.1 } })。 - 复杂度要求:非
O(1)复杂度的计算属性需在文档中注明,避免调用者忽视性能损耗。 - 适用类型:类、结构体、枚举,可定义在类型本体或扩展中。
- 本质:无独立存储空间,本质是“无参数方法”,通过
(二)变更观察者(WillSet & DidSet)
- 核心作用:监控属性值变化(即使值未改变也会触发),分别在“值设置前”(
willSet)和“值设置后”(didSet)执行。 - 关键特性:
willSet:参数newValue表示新值,可自定义名称(如willSet(newVal) { ... })。didSet:参数oldValue表示旧值,常用于触发副作用(如didSet { setNeedsLayout() })。- 限制:必须在属性声明时定义,不能在扩展中添加;子类可重写父类属性以追加观察者。
- 与KVO的区别:Swift变更观察者是编译时特性,仅作用于类型内部;Objective-C KVO是运行时特性,支持外部观察。
(三)延迟存储属性(Lazy Property)
- 本质:延迟初始化,首次访问时才执行初始化逻辑,初始化后值被缓存。
- 语法与限制:
- 必须用
var声明(let需初始化完成前赋值,无法延迟)。 - 初始化逻辑:支持闭包表达式(如
lazy var preview: UIImage = { /* 耗时计算 */ }()),闭包后需加()触发执行。 - 内存特性:结构体中访问延迟属性属于
mutating操作,需将结构体实例声明为var(因访问时可能修改实例内部状态)。 - 线程安全:无内置线程同步,多线程同时访问可能导致初始化逻辑执行多次。
- 必须用
- 注意事项:延迟属性的值不会随依赖属性变化自动更新(如
distanceFromOrigin依赖x/y,x修改后需手动重新计算)。
(四)属性包装(Property Wrapper)
- 核心目标:将属性的重复逻辑(如存储、验证、副作用)封装为可复用组件,简化代码(替代重复的
willSet/didSet或计算属性逻辑)。 - 基础实现:
- 关键字
@propertyWrapper标记封装类型,必须实现wrappedValue(get必需,set可选)。 - 编译器转换:为被包装属性生成“带下划线的私有存储属性”(如
@Box var isOn: Bool生成private var _isOn: Box<Bool>),通过wrappedValue访问实际值。 - 初始化:支持
init(wrappedValue:)默认初始化(如Box的init(wrappedValue: A)),允许直接赋值初始值(@Box var isOn: Bool = false)。
- 关键字
- 核心特性:
- 投影值(Projected Value):通过
projectedValue属性暴露额外功能,访问时加$前缀(如$person对应person.projectedValue),常用于绑定或引用传递。 - Self封装:部分属性包装需访问“封装实例”(如
@Invalidating需调用view.setNeedsLayout()),通过静态下标static subscript(_enclosingInstance: ...)实现。 - 适用场景:状态管理(SwiftUI的
@State/@Binding)、依赖注入(@Inject)、副作用触发(@Invalidating(.layout))、存储适配(@UserDefaults)。
- 投影值(Projected Value):通过
- 使用限制:
- 不能用于枚举(枚举无实例存储)、全局变量;可用于类、结构体、局部变量、函数参数。
- 不可与
weak/unowned/lazy/@NSCopying同时修饰。 - 嵌套使用需注意:仅外层包装支持“封装实例访问”(如SwiftUI的
@State需作为最外层包装)。
(五)键路径(Key Path)
- 本质与语法:
- 定义:指向属性的“未调用引用”,语法以
\开头(如\Person.address.street),支持类型推断(上下文明确时可简写为\.count)。 - 核心价值:类型安全、无拼写错误风险,区别于Objective-C的字符串键路径(如
"address.street")。
- 定义:指向属性的“未调用引用”,语法以
- 键路径类型层级(从基础到复杂)
AnyKeyPath:最基础类型,类似(Any) -> Any?,无类型信息。PartialKeyPath<Source>:绑定源类型,值类型为Any?(如PartialKeyPath<Person>)。KeyPath<Source, Target>:强类型键路径,对应(Source) -> Target(如\Person.name为KeyPath<Person, String>)。WritableKeyPath<Source, Target>:可写键路径,支持修改属性值,要求所有嵌套属性均为var(如\Person.address.street)。ReferenceWritableKeyPath<Source, Target>:针对引用类型的可写键路径,无需inout参数即可修改(如UIView的属性键路径)。
- 核心操作与应用
- 访问属性:通过
instance[keyPath: keyPath]访问(如lisa[keyPath: \.name])。 - 键路径拼接:通过
append(path:)组合键路径(如\Person.name+\String.count=KeyPath<Person, Int>)。 - 支持下标:可包含数组、字典下标(如
\.[1].name访问数组第二个元素的name属性)。 - 与函数的转换:编译器自动将键路径转为函数(如
people.map(\.name)等价于people.map { $0.name })。
- 访问属性:通过
- 与Objective-C键路径的区别
- Swift键路径:编译时类型检查、无拼写错误、支持
Hashable(可作为字典键)、无状态。 - Objective-C键路径:字符串形式、运行时解析、可能崩溃、无类型安全。
- Swift键路径:编译时类型检查、无拼写错误、支持
(六)属性的其他关键细节
- 局部变量与全局变量:局部变量支持属性包装,全局变量不支持;变量与属性遵循相同的可变性、观察者规则。
- 函数参数中的属性包装:Swift 5.5+支持函数参数使用属性包装(如
func takesBox(@Box foo: String)),编译器自动生成包装逻辑。 - 属性包装的弃用与错误处理:可通过
@available(*, unavailable, message: ...)标记不支持的类型,提供清晰编译错误。
二、重点知识点总结
(一)属性的本质区别与使用场景
- 存储属性vs计算属性:
- 存储属性:适用于需长期保存数据、直接访问的场景(如
var record: [(CLLocation, Date)]),注意访问控制与可变性控制。 - 计算属性:适用于“动态计算值”“无独立存储”的场景(如
var timestamps: [Date]),只读计算属性优先省略set方法。
- 存储属性:适用于需长期保存数据、直接访问的场景(如
- 核心原则:避免将复杂计算逻辑放入存储属性,通过计算属性或延迟存储属性拆分;非
O(1)复杂度的计算属性必须在文档中注明。
(二)属性包装的核心价值与实用范式
- 简化重复逻辑:将“值存储+副作用”(如属性变更触发布局失效)封装为属性包装(如
@Invalidating(.layout)),替代冗余的didSet。 - 关键语法糖应用:
wrappedValue:对外暴露的“透明接口”,使用时与普通属性无差异。projectedValue:通过$前缀访问,适用于绑定(如SwiftUI的$isOn)、引用传递等场景。
- 常见应用场景:
- UI开发:
@Invalidating触发布局/约束失效。 - 状态管理:SwiftUI的
@State/@Binding/@Published。 - 存储适配:
@UserDefaults包装偏好设置存储。
- UI开发:
(三)键路径的核心优势与使用技巧
- 简洁性与类型安全:相比闭包,键路径语法更简洁(如
\Person.namevs{ $0.name }),且编译时检查类型,避免运行时错误。 - 关键操作:
- 可写键路径:
WritableKeyPath用于值类型属性修改,ReferenceWritableKeyPath用于引用类型,无需inout参数。 - 键路径拼接:通过
append(path:)组合多层属性,灵活构建复杂路径。
- 可写键路径:
- 与函数的区别:键路径是
Hashable、无状态的,可存储、比较;函数不可比较,可能捕获状态,两者不能双向转换。
(四)变更观察者与延迟属性的实用场景
- 变更观察者:适用于“属性变更需触发副作用”的场景(如
pageSize变更触发setNeedsLayout),注意不能在扩展中添加,仅能在属性声明时定义。 - 延迟属性:适用于“初始化耗时”“按需加载”的场景(如预览图生成),避免初始化时浪费资源;结构体中使用需注意
var声明要求。
三、难点知识点总结
(一)属性包装的底层原理与限制
- 编译器转换逻辑:被包装属性会生成“带下划线的私有存储属性”(如
@Box var isOn生成_isOn: Box<Bool>),wrappedValue是访问该存储属性的计算属性,容易忽略底层包装逻辑导致误用。 - 投影值的混淆点:
$前缀对应的projectedValue需手动实现,默认无投影值;部分系统包装(如@State)的投影值是Binding类型,需区分“属性值”与“绑定值”。 - 使用限制的坑:
- 不能用于枚举、全局变量,函数参数中的属性包装会暴露包装类型(如
init(projectedValue:)存在时)。 - 嵌套包装仅外层支持“封装实例访问”,内层包装无法获取
_enclosingInstance。
- 不能用于枚举、全局变量,函数参数中的属性包装会暴露包装类型(如
(二)键路径的类型区分与使用陷阱
- 可写键路径的两种类型:
WritableKeyPath:适用于值类型,修改需实例为var(如结构体属性)。ReferenceWritableKeyPath:适用于引用类型,修改无需实例为var(如类属性),容易混淆两者的适用场景。
- 与函数的差异:键路径是
Hashable、无状态的,可作为字典键;函数不可比较、可能捕获状态,不能直接替代键路径(如数据库查询中的属性引用)。 - 性能问题:相比直接访问属性,键路径访问速度较慢,避免在性能敏感场景(如密集循环)中频繁使用。
(三)延迟存储属性的特殊行为
- 结构体中的限制:结构体的延迟属性访问属于
mutating操作,即使属性本身是let,也需将结构体实例声明为var,否则编译报错(容易忽视“访问即修改”的隐式行为)。 - 线程安全风险:无内置线程同步,多线程同时首次访问可能导致初始化逻辑执行多次,需手动添加锁或避免多线程并发访问。
- 值缓存问题:延迟属性初始化后值被缓存,依赖属性变更不会触发重新计算(如
distanceFromOrigin依赖x/y,x修改后仍返回旧值)。
(四)属性相关的编译器特性与隐式行为
- 变更观察者的触发时机:即使属性值未改变(如
x = x),willSet和didSet仍会触发;inout参数传递会强制触发观察者(即使未修改值)。 - 属性包装的自动初始化:
init(wrappedValue:)是可选的,若未实现则无法直接赋值初始值(如@Reference var street: String需通过init(projectedValue:)初始化)。 - 键路径的类型推断歧义:
\Person.name可推断为KeyPath<Person, String>或(Person) -> String,编译器优先推断为键路径类型,需显式标注类型解决歧义。
四、总结
本章核心围绕“属性的本质与高效使用”展开,核心逻辑是“区分存储与计算的本质差异,利用属性包装和键路径简化重复逻辑、提升代码安全性”。重点在于掌握属性的分类特性、属性包装的实用范式、键路径的类型安全优势;难点集中在属性包装的底层转换、键路径的类型区分、延迟属性的特殊限制,以及属性相关的隐式编译器行为。
实际开发中,应根据场景选择属性类型:存储属性用于保存数据,计算属性用于动态计算,属性包装用于封装重复逻辑(如状态管理、副作用触发),键路径用于简洁、类型安全的属性引用。同时需规避常见陷阱(如结构体中延迟属性的var要求、属性包装的使用限制),平衡代码简洁性与可维护性。
如果需要,我可以帮你整理属性包装核心API的对比表,或针对某个难点(如键路径与函数的转换、属性包装的自定义实现)提供详细代码示例。当前文件内容过长,豆包只阅读了前 16%。