本文概述
- 旧方法
- 新方法propertyWrapper注解
- 可配置包装器
- 自行访问包装器
- 投影值
- 局限性
- 结论
简而言之, 属性包装器是一种通用结构, 它封装了对该属性的读写访问, 并为其添加了其他行为。如果需要限制可用的属性值, 向读/写访问添加额外的逻辑例如使用数据库或用户默认值或添加一些其他方法, 则可以使用它。 本文介绍了一种新的Swift 5.1包装属性的方法, 该方法引入了一种更简洁的新语法。
旧方法
假设你正在开发应用程序, 并且有一个包含用户配置文件数据的对象。
struct Account {var firstName: Stringvar lastName: Stringvar email: String?}let account Account(firstName: "Test", lastName: "Test", email: "[email protected]")account.email "[email protected]"print(account.email)
你要添加电子邮件验证-如果用户电子邮件地址无效, 则email属性必须为nil。使用属性包装器封装此逻辑将是一个很好的情况。
struct Email {private var _value: Value?init(initialValue value: Value?) {_value value}var value: Value? {get {return validate(email: _value) ? _value : nil}set {_value newValue}}private func validate(email: Value?) -> Bool {guard let email email else { return false }let regex "[A-Z0-9a-z._%-][email protected][A-Za-z0-9.-]\\.[A-za-z]{2, 64}"let pred NSPredicate(format: "SELF MATCHES %", regex)return pred.evaluate(with: email)}}
我们可以在Account结构中使用此包装器
struct Account {var firstName: Stringvar lastName: Stringvar email: Email}
现在, 我们确定email属性只能包含有效的电子邮件地址。 除了语法外, 其他一切看起来都不错。
let account Account(firstName: "Test", lastName: "Test", email: Email(initialValue: "[email protected]"))account.email.value "[email protected]"print(account.email.value)
使用属性包装器, 用于初始化, 读取和写入此类属性的语法变得更加复杂。因此, 是否有可能避免这种麻烦并在不更改语法的情况下使用属性包装器使用Swift 5.1, 答案是肯定的。
新方法propertyWrapper注解
Swift 5.1为创建属性包装器提供了更为优雅的解决方案, 其中允许使用propertyWrapper注解标记属性包装器。与传统的包装器相比, 此类包装器具有更紧凑的语法, 从而使代码更紧凑和易于理解。 propertyWrapper批注仅具有一个要求包装器对象必须包含一个称为被包装的值的非静态属性。
propertyWrapperstruct Email {var value: Value?var wrappedValue: Value? {get {return validate(email: value) ? value : nil}set {value newValue}}private func validate(email: Value?) -> Bool {guard let email email else { return false }let emailRegEx "[A-Z0-9a-z._%-][email protected][A-Za-z0-9.-]\\.[A-Za-z]{2, 64}"let emailPred NSPredicate(format:"SELF MATCHES %", emailRegEx)return emailPred.evaluate(with: email)}}
要在代码中定义这种包装的属性, 我们需要使用新的语法。
Emailvar email: String?
因此, 我们用注解标记了该属性。属性类型必须与包装器的“ wrappedValue”类型匹配。现在, 你可以像使用普通属性一样使用此属性。
email "[email protected]"print(email) // [email protected]email "invalid"print(email) // nil
太好了, 现在看起来比以前的方法更好。但是我们的包装器实现有一个缺点不允许为包装后的值提供初始值。
Emailvar email: String? "[email protected]" //compilation error.
要解决此问题, 我们需要在包装器中添加以下初始化程序
init(wrappedValue value: Value?) {self.value value}
就是这样。
Emailvar email: String? "[email protected]"print(email) // [email protected]Emailvar email: String? "invalid"print(email) // nil
包装程序的最终代码如下
propertyWrapperstruct Email {var value: Value?init(wrappedValue value: Value?) {self.value value}var wrappedValue: Value? {get {return validate(email: value) ? value : nil}set {value newValue}}private func validate(email: Value?) -> Bool {guard let email email else { return false }let emailRegEx "[A-Z0-9a-z._%-][email protected][A-Za-z0-9.-]\\.[A-Za-z]{2, 64}"let emailPred NSPredicate(format:"SELF MATCHES %", emailRegEx)return emailPred.evaluate(with: email)}}
可配置包装器
让我们再举一个例子。你正在编写游戏, 并且具有存储用户分数的属性。要求此值应大于或等于0且小于或等于100。你可以使用属性包装器来实现。
propertyWrapperstruct Scores {private let minValue 0private let maxValue 100private var value: Intinit(wrappedValue value: Int) {self.value value}var wrappedValue: Int {get {return max(min(value, maxValue), minValue)}set {value newValue}}}Scoresvar scores: Int 0
该代码有效, 但似乎并不通用。你不能在不同的限制不能为0和100下重复使用它。而且, 它只能约束整数值。最好有一个可配置的包装器, 它可以约束符合Comparable协议的任何类型。为了使包装器可配置, 我们需要通过初始化程序添加所有配置参数。如果初始化程序包含包装的属性属性的初始值, 则它必须是第一个参数。
propertyWrapperstruct Constrained {private var range: ClosedRangeprivate var value: Valueinit(wrappedValue value: Value, _ range: ClosedRange) {self.value valueself.range range}var wrappedValue: Value {get {return max(min(value, range.upperBound), range.lowerBound)}set {value newValue}}}
要初始化包装的属性, 我们在注释后的括号中定义所有配置属性。
Constrained(0...100)var scores: Int 0
配置属性的数量是无限的。你需要以与初始化程序相同的顺序在括号中定义它们。
自行访问包装器
如果需要访问包装器本身而不是包装的值, 则需要在属性名称之前添加下划线。例如, 让我们采用“帐户”结构。
struct Account {var firstName: Stringvar lastName: StringEmailvar email: String?}let account Account(firstName: "Test", lastName: "Test", email: "[email protected]")account.email // Wrapped value (String)account._email // Wrapper(Email)
为了使用添加到包装器中的其他功能, 我们需要访问包装器本身。例如, 我们希望Account结构符合Equatable协议。如果两个帐户的电子邮件地址相等, 则两个帐户相等, 并且电子邮件地址必须区分大小写。
extension Account: Equatable {static func (lhs: Account, rhs: Account) -> Bool {return lhs.email?.lowercased() rhs.email?.lowercased()}}
它可以工作, 但不是最佳解决方案, 因为无论何时比较电子邮件, 我们都必须记住添加一个lowercased()方法。更好的方法是使Email结构相等
extension Email: Equatable {static func (lhs: Email, rhs: Email) -> Bool {return lhs.wrappedValue?.lowercased() rhs.wrappedValue?.lowercased()}}
并比较包装器而不是包装的值
extension Account: Equatable {static func (lhs: Account, rhs: Account) -> Bool {return lhs._email rhs._email}}
投影值
propertyWrapper批注提供了另一种语法糖-投影值。该属性可以具有你想要的任何类型。要访问此属性, 你需要在属性名称中添加$前缀。为了解释它是如何工作的, 我们使用Combine框架中的示例。 Published属性包装器为该属性创建一个发布者, 并将其作为投影值返回。
Publishedvar message: Stringprint(message) // Print the wrapped value$message.sink { print($0) } // Subscribe to the publisher
如你所见, 我们使用一条消息来访问包装的属性, 并使用$message来访问发布者。你应该怎么做才能为包装器添加预计的价值没什么特别的, 只需声明一下即可。
propertyWrapperstruct Published {private let subject PassthroughSubject()var wrappedValue: Value {didSet {subject.send(wrappedValue)}}var projectedValue: AnyPublisher {subject.eraseToAnyPublisher()}}
如前所述, projectedValue属性可以根据你的需要具有任何类型。
局限性
新的属性包装器的语法看起来不错, 但它也包含一些限制, 主要限制是
- 他们无法参与错误处理 包装的值是一个属性不是方法, 我们不能将getter或setter标记为throws。例如, 在我们的电子邮件示例中, 如果用户尝试设置无效的电子邮件, 则不可能引发错误。我们可以返回nil或通过fatalError()调用使应用程序崩溃, 这在某些情况下是不可接受的。
- 不允许对属性应用多个包装 例如, 最好有一个单独的CaseInsensitive包装器, 并将其与Email包装器组合, 而不是使Email包装器不区分大小写。但是这样的构造是被禁止的, 并且会导致编译错误。
CaseInsensitiveEmailvar email: String?
作为此特定情况的解决方法, 我们可以从CaseInsensitive包装器继承Email包装器。但是, 继承也有局限性-只有类支持继承, 并且只允许一个基类。
结论
propertyWrapper注释简化了属性包装器的语法, 并且我们可以使用与普通属性相同的方式来处理包装的属性。这使你作为Swift开发人员的代码更加紧凑和易于理解。同时, 它有一些必须考虑的限制。我希望其中一些会在以后的Swift版本中得到纠正。 如果你想了解有关Swift属性的更多信息, 请查看官方文档。