5月27日 11:26

Swift 属性包装器 @propertyWrapper 怎么用?有什么局限?

属性包装器 @propertyWrapper 把属性的 get/set 逻辑抽出来封装成可复用的类型。比如你有一堆属性都需要做范围限制、线程安全、UserDefaults 存取——不写包装器的话,每个属性的 getter/setter 里都要写一遍相同的逻辑。

属性包装器的核心是 wrappedValue:它替代了原始属性的存取逻辑。外部访问属性时,实际访问的是包装器的 wrappedValue。包装器还可以通过 projectedValue(用 $ 前缀访问)提供额外功能,比如标记值是否被裁剪过。

swift
@propertyWrapper struct Clamped<Value: Comparable> { var value: Value let range: ClosedRange<Value> var wrappedValue: Value { get { value } set { value = min(max(newValue, range.lowerBound), range.upperBound) } } var projectedValue: Bool { value != value // 是否被裁剪 } } struct Player { @Clamped(range: 0...100) var health: Int = 100 }

追问

属性包装器的 wrappedValue 和 projectedValue 有什么区别?

wrappedValue 是属性的值本身,通过属性名直接访问。projectedValue 是包装器暴露的额外信息,通过 $ 前缀访问。比如 @UserDefault 的 wrappedValue 是存储的值,$someKey 可以返回一个 Publisher 用于响应式监听。projectedValue 不是必须的,大部分简单包装器只实现 wrappedValue。

属性包装器能替代 willSet/didSet 吗?

能,而且更灵活。willSet/didSet 是写在属性定义里的,每个属性重复一遍;属性包装器把同样的逻辑封装成类型,多个属性复用。但属性包装器比 willSet/didSet 复杂得多——如果只是简单的值变化通知,willSet/didSet 更直接。需要复用逻辑时才用包装器。

SwiftUI 里常用的属性包装器有哪些?各自的作用?

  • @State:值类型本地状态,视图独占
  • @Binding:父子的双向数据绑定
  • @ObservedObject:外部传入的引用类型观察对象
  • @StateObject:视图自己创建和持有的观察对象
  • @EnvironmentObject:从环境注入的共享对象
  • @Published:在 ObservableObject 里标记属性变化时自动通知

这些本质上都是属性包装器,但 SwiftUI 框架为它们注入了特殊的依赖追踪和视图刷新机制。

属性包装器有什么局限?

不能包装 lazy 属性和带观察器的属性——它们对存储方式有特殊要求,和包装器冲突。包装器的 init 不能访问 enclosing instance(所在类型的实例),所以包装器内部没法直接调用所在类型的方法。另外,包装器增加了间接调用层级,极端性能场景下可能有微小的额外开销。

写段代码

swift
@propertyWrapper struct UserDefault<T> { let key: String let defaultValue: T var wrappedValue: T { get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } } } struct Settings { @UserDefault(key: "darkMode", defaultValue: false) var darkMode: Bool @UserDefault(key: "fontSize", defaultValue: 14) var fontSize: Int } var settings = Settings() settings.darkMode = true // 自动写入 UserDefaults print(settings.darkMode) // 自动从 UserDefaults 读取
标签:Swift