如何处理Swift属性的包装器

本文概述

  • 旧方法
  • 新方法:@propertyWrapper注释
  • 可配置包装器
  • 自行访问包装器
  • 投影值
  • 局限性
  • 结论
简而言之, 属性包装器是一种通用结构, 它封装了对该属性的读写访问, 并为其添加了其他行为。如果需要限制可用的属性值, 向读/写访问添加额外的逻辑(例如使用数据库或用户默认值)或添加一些其他方法, 则可以使用它。
如何处理Swift属性的包装器

文章图片
本文介绍了一种新的Swift 5.1包装属性的方法, 该方法引入了一种更简洁的新语法。
旧方法假设你正在开发应用程序, 并且有一个包含用户配置文件数据的对象。
struct Account { var firstName: String var lastName: String var email: String? }let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]")account.email = "[email protected]" print(account.email)

你要添加电子邮件验证-如果用户电子邮件地址无效, 则email属性必须为nil。使用属性包装器封装此逻辑将是一个很好的情况。
struct Email< Value: StringProtocol> { private var _value: Value?init(initialValue value: Value?) { _value = http://www.srcmini.com/value }var value: Value? { get { return validate(email: _value) ? _value : nil }set { _value = http://www.srcmini.com/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: String var lastName: String var email: Email< String> }

现在, 我们确定email属性只能包含有效的电子邮件地址。
除了语法外, 其他一切看起来都不错。
let account = Account(firstName: "Test", lastName: "Test", email: Email(initialValue: "[email protected]"))account.email.value = "http://www.srcmini.com/[email protected]" print(account.email.value)

使用属性包装器, 用于初始化, 读取和写入此类属性的语法变得更加复杂。因此, 是否有可能避免这种麻烦并在不更改语法的情况下使用属性包装器?使用Swift 5.1, 答案是肯定的。
新方法:@propertyWrapper注释Swift 5.1为创建属性包装器提供了更为优雅的解决方案, 其中允许使用@propertyWrapper注释标记属性包装器。与传统的包装器相比, 此类包装器具有更紧凑的语法, 从而使代码更紧凑和易于理解。 @propertyWrapper批注仅具有一个要求:包装器对象必须包含一个称为被包装的值的非静态属性。
@propertyWrapper struct Email< Value: StringProtocol> { var value: Value?var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = http://www.srcmini.com/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) } }

要在代码中定义这种包装的属性, 我们需要使用新的语法。
@Email var email: String?

因此, 我们用注解@标记了该属性。属性类型必须与包装器的“ wrappedValue”类型匹配。现在, 你可以像使用普通属性一样使用此属性。
email = "[email protected]" print(email) // [email protected] email = "invalid" print(email) // nil

太好了, 现在看起来比以前的方法更好。但是我们的包装器实现有一个缺点:不允许为包装后的值提供初始值。
@Email var email: String? = "[email protected]" //compilation error.

要解决此问题, 我们需要在包装器中添加以下初始化程序:
init(wrappedValue value: Value?) { self.value = http://www.srcmini.com/value }

就是这样。
@Email var email: String? = "[email protected]" print(email) // [email protected]@Email var email: String? = "invalid" print(email) // nil

包装程序的最终代码如下:
@propertyWrapper struct Email< Value: StringProtocol> { var value: Value? init(wrappedValue value: Value?) { self.value = value } var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = http://www.srcmini.com/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。你可以使用属性包装器来实现。
@propertyWrapper struct Scores { private let minValue = http://www.srcmini.com/0 private let maxValue = 100 private var value: Int init(wrappedValue value: Int) { self.value = value } var wrappedValue: Int { get { return max(min(value, maxValue), minValue) } set { value = newValue } } }@Scores var scores: Int = 0

该代码有效, 但似乎并不通用。你不能在不同的限制(不能为0和100)下重复使用它。而且, 它只能约束整数值。最好有一个可配置的包装器, 它可以约束符合Comparable协议的任何类型。为了使包装器可配置, 我们需要通过初始化程序添加所有配置参数。如果初始化程序包含包装的属性(属性的初始值), 则它必须是第一个参数。
@propertyWrapper struct Constrained< Value: Comparable> { private var range: ClosedRange< Value> private var value: Value init(wrappedValue value: Value, _ range: ClosedRange< Value>) { self.value = http://www.srcmini.com/value self.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: String var lastName: String @Email var email: String? }let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]")account.email // Wrapped value (String) account._email // Wrapper(Email< String>)

为了使用添加到包装器中的其他功能, 我们需要访问包装器本身。例如, 我们希望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属性包装器为该属性创建一个发布者, 并将其作为投影值返回。
@Published var message: Stringprint(message) // Print the wrapped value $message.sink { print($0) } // Subscribe to the publisher

如你所见, 我们使用一条消息来访问包装的属性, 并使用$ message来访问发布者。你应该怎么做才能为包装器添加预计的价值?没什么特别的, 只需声明一下即可。
@propertyWrapper struct Published< Value> { private let subject = PassthroughSubject< Value, Never>() var wrappedValue: Value { didSet { subject.send(wrappedValue) } } var projectedValue: AnyPublisher< Value, Never> { subject.eraseToAnyPublisher() } }

如前所述, projectedValue属性可以根据你的需要具有任何类型。
局限性新的属性包装器的语法看起来不错, 但它也包含一些限制, 主要限制是:
  1. 他们无法参与错误处理。包装的值是一个属性(不是方法), 我们不能将getter或setter标记为throws。例如, 在我们的电子邮件示例中, 如果用户尝试设置无效的电子邮件, 则不可能引发错误。我们可以返回nil或通过fatalError()调用使应用程序崩溃, 这在某些情况下是不可接受的。
  2. 不允许对属性应用多个包装。例如, 最好有一个单独的@CaseInsensitive包装器, 并将其与@Email包装器组合, 而不是使@Email包装器不区分大小写。但是这样的构造是被禁止的, 并且会导致编译错误。
@CaseInsensitive @Email var email: String?

作为此特定情况的解决方法, 我们可以从CaseInsensitive包装器继承电子邮件包装器。但是, 继承也有局限性-只有类支持继承, 并且只允许一个基类。
结论@propertyWrapper注释简化了属性包装器的语法, 并且我们可以使用与普通属性相同的方式来处理包装的属性。这使你作为Swift开发人员的代码更加紧凑和易于理解。同时, 它有一些必须考虑的限制。我希望其中一些会在以后的Swift版本中得到纠正。
【如何处理Swift属性的包装器】如果你想了解有关Swift属性的更多信息, 请查看官方文档。

    推荐阅读