本文概述
简而言之, 属性包装器是一种通用结构, 它封装了对该属性的读写访问, 并为其添加了其他行为。如果需要限制可用的属性值, 向读/写访问添加额外的逻辑(例如使用数据库或用户默认值)或添加一些其他方法, 则可以使用它。
本文介绍了一种新的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 = 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: String
var lastName: String
var email: Email<String>
}
现在, 我们确定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批注仅具有一个要求:包装器对象必须包含一个称为被包装的值的非静态属性。
@propertyWrapper
struct Email<Value: StringProtocol> {
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)
}
}
要在代码中定义这种包装的属性, 我们需要使用新的语法。
@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 = 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 = 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 = 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 = 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: String
print(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属性可以根据你的需要具有任何类型。
局限性
新的属性包装器的语法看起来不错, 但它也包含一些限制, 主要限制是:
- 他们无法参与错误处理。包装的值是一个属性(不是方法), 我们不能将getter或setter标记为throws。例如, 在我们的电子邮件示例中, 如果用户尝试设置无效的电子邮件, 则不可能引发错误。我们可以返回nil或通过fatalError()调用使应用程序崩溃, 这在某些情况下是不可接受的。
- 不允许对属性应用多个包装。例如, 最好有一个单独的@CaseInsensitive包装器, 并将其与@Email包装器组合, 而不是使@Email包装器不区分大小写。但是这样的构造是被禁止的, 并且会导致编译错误。
@CaseInsensitive
@Email
var email: String?
作为此特定情况的解决方法, 我们可以从CaseInsensitive包装器继承电子邮件包装器。但是, 继承也有局限性-只有类支持继承, 并且只允许一个基类。
结论
@propertyWrapper注释简化了属性包装器的语法, 并且我们可以使用与普通属性相同的方式来处理包装的属性。这使你作为Swift开发人员的代码更加紧凑和易于理解。同时, 它有一些必须考虑的限制。我希望其中一些会在以后的Swift版本中得到纠正。
如果你想了解有关Swift属性的更多信息, 请查看官方文档。