Swift属性的包装器

ReyZhang
移动开发领域新星创作者
2023-02-17 10:13:08
加精

简而言之, 属性包装器是一种通用结构, 它封装了对该属性的读写访问, 并为其添加了其他行为。如果需要限制可用的属性值, 向读/写访问添加额外的逻辑(例如使用数据库或用户默认值)或添加一些其他方法, 则可以使用它。
本文介绍了一种新的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包装器继承@Email包装器。但是, 继承也有局限性-只有类支持继承, 并且只允许一个基类

结论

@propertyWrapper注释简化了属性包装器的语法, 并且我们可以使用与普通属性相同的方式来处理包装的属性。这使你作为Swift开发人员的代码更加紧凑和易于理解。同时, 它有一些必须考虑的限制。我希望其中一些会在以后的Swift版本中得到纠正。
如果你想了解有关Swift属性的更多信息, 请查看官方文档。

...全文
成就一亿技术人!
拼手气红包 5.00元
803 回复 打赏 收藏 转发到动态 举报
写回复
回复
切换为时间正序
请发表友善的回复…
发表回复
相关推荐

404

社区成员

发帖
与我相关
我的任务
社区描述
专注移动ios平台的软件开发,多年的一线研发经验,实战经验丰富,只为你呈现有价值的信息。
iosflutterandroid 技术论坛(原bbs) 山东省·青岛市
社区管理员
  • ReyZhang
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告