Swift 5.1通过SE-0258引入了属性包装器,以消除重复的访问器样板代码。projectedValue的要求旨在暴露次级API表面——例如SwiftUI的Binding或验证状态——超出包装值本身。此功能允许开发人员使用$前缀语法访问元数据或投影。
问题在于,Swift必须将声明性语法转换为有效的SIL(Swift中间语言),而不引入名称冲突或破坏访问控制。编译器需要合成保持包装属性值语义的存储,同时可能通过投影暴露引用语义,并确保$前缀标识符不与用户定义的成员冲突。
解决方案涉及源到源去糖化。对于声明为@Wrapper var property: T的属性,编译器生成三个不同的成员。首先,一个类型为Wrapper<T>的私有存储变量_property。其次,一个计算属性property,将get/set操作转发到_property.wrappedValue。第三,一个计算属性$property,返回_property.projectedValue。带有$前缀的属性继承原始声明的访问控制,编译器强制确保在使用$语法时存在projectedValue。
@propertyWrapper struct Validating<T> { var wrappedValue: T var projectedValue: ValidationState<T> init(wrappedValue: T) { self.wrappedValue = wrappedValue self.projectedValue = ValidationState(value: wrappedValue) } } // 去糖化为: struct Form { private var _username: Validating<String> var username: String { get { _username.wrappedValue } set { _username.wrappedValue = newValue } } var $username: ValidationState<String> { get { _username.projectedValue } } }
我们在构建一个医疗数据输入应用程序,其中每个字段需要跟踪其当前值和复杂的验证历史记录,包括先前错误和修正时间戳。这个挑战需要从单一的属性抽象中暴露两条不同的数据路径:用于UI文本字段的原始字符串和用于分析和错误显示的验证历史记录。
考虑的第一个方法是维持一个并行字典,将属性名称映射到ValidationHistory对象。这提供了存储的灵活性,但引入了在重构期间会崩溃的字符串类型API,并需要在字典和实际属性值之间手动同步。由于医疗数据的过期错误显示的风险过高,这是不可接受的。
第二个方法涉及创建一个包装结构,其中包含值和历史,然后使用该复合类型作为属性类型。虽然是类型安全的,但这污染了领域模型,带来了验证问题,并迫使每个访问点处理解包,这违背了UI与业务逻辑之间清晰架构分离的目的。
第三个方法使用了一个自定义的@Validated属性包装器,其projectedValue返回一个ValidationHistory引用类型。这在内部封装了同步,同时暴露了$fieldName用于历史访问。我们选择了这个方法,因为它保持了CoW(写时复制)语义,用于包装字符串值,同时为验证历史提供稳定的引用标识,确保UI组件可以在没有复制开销的情况下观察更改。
结果消除了整个类的同步错误,减少了与验证相关的代码量35%。$语法为初级开发者提供了直观的可发现性,编译时强制执行防止意外暴露实现细节跨模块边界。
为什么通过美元符号前缀访问的值类型的projectedValue的变更不会持久存在?
当属性包装器是一个结构体时,projectedValue获取器返回值的副本。如果projectedValue返回一个结构体(例如Int或自定义的验证状态结构),则语句如$property.errorCount += 1会改变一个临时副本,这个副本会立即被丢弃。为了实现持久变化,projectedValue必须返回一个引用类型,或者包装器必须实现CoW,并具有类基础存储。或者,返回一个提供间接访问的Binding或可变指针。初学者常假设$property提供对包装器内部状态的可变访问,而未考虑Swift的值语义。
合成的带有美元符号的属性的访问控制如何与原始属性的访问级别相互作用?
编译器以与原始属性相同的访问控制合成带有$前缀的属性。如果您声明public @Wrapper var name: String,则name和$name都是public。相反,private属性生成private投影值。候选人常常试图使包装值为public,同时保持投影值为internal或private,而这是不可能的在当前的Swift版本中。解决方法要求将属性设为private,并通过显式的计算属性公开包装值,而投影值仍然受限。
单个属性包装器能否暴露多个不同的投影,且有何消费影响?
Swift严格允许每个包装器仅有一个projectedValue属性。然而,该属性可以返回一个元组、结构或枚举,包含多个值(例如,projectedValue: (Binding<T>, ValidationError?, Bool))。消费上的权衡是$property因此需要点语法访问组件($property.0,$property.isValid),从而降低可读性。一些候选人试图声明多个projectedValue属性或将多个属性包装器应用于同一属性(链接)。虽然链接是被支持的,但它会创建复杂的初始化语义和不透明的类型推导问题。推荐的处理多个投影的方法是返回一个专用的投影结构,具有命名属性,保持类型安全,同时接受语法开销。