作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Aleksandr Gaidukov的头像

Aleksandr Gaidukov

Alexander有超过9年的应用程序开发经验,并有超过5年的iOS平台开发经验,包括iPhone和iPad.

Expertise

Previously At

Accenture
Share

In simple terms, 属性包装器是一种通用结构,它封装对属性的读写访问,并向其添加附加行为. 如果需要约束可用的属性值,则使用它, 为读/写访问添加额外的逻辑(比如使用数据库或用户默认值), 或者添加一些额外的方法.

Swift 5中的属性包装器.1

这篇文章是关于新的Swift 5的.一种包装属性的方法,它引入了一种新的、更简洁的语法.

Old Approach

假设您正在开发一个应用程序,并且您有一个包含用户配置文件数据的对象.

struct Account {
    var firstName: String
    var lastName: String
    var email: String?
}

let account = account (firstName: "Test"),
                      lastName: "Test",
                      email: "test@test.com")

account.email = "new@test.com"
print(account.email)

您需要添加电子邮件验证—如果用户电子邮件地址无效,则 email property must be nil. 这将是使用属性包装器封装此逻辑的好情况.

struct Email {
    private var _value:取值?
    
    init(initialValue) value: value?) {
        _value = value
    }
    
    var value: Value? {
        get {
            返回validate(email: _value) ? _value : nil
        }
        
        set {
            _value = newValue
        }
    }
    
    private function validate(email: Value?) -> Bool {
        守卫让email = email else{返回false}
        let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64}"
        let pred = NSPredicate(format: "SELF MATCHES %@",正则表达式)
        return pred.evaluate(with: email)
    }
}

我们可以在Account结构中使用这个包装器:

struct Account {
    var firstName: String
    var lastName: String
    var email: Email
}

现在,我们确定email属性只能包含有效的电子邮件地址.

一切看起来都很好,除了语法.

let account = account (firstName: "Test"),
                      lastName: "Test",
                      email: email (initialValue:“test@test”).com"))

account.email.value = "new@test.com"
print(account.email.value)

使用属性包装器, 初始化的语法, reading, 编写这样的属性变得更加复杂. 那么,是否有可能避免这种复杂性并在不更改语法的情况下使用属性包装器? With Swift 5.1, the answer is yes.

新方法:@propertyWrapper注释

Swift 5.1 提供创建属性包装器的更优雅的解决方案, 在标记属性包装器时使用 @propertyWrapper annotation is allowed. 与传统包装器相比,这种包装器具有更紧凑的语法, 从而产生更紧凑和可理解的代码. The @propertyWrapper 注释只有一个要求:包装器对象必须包含一个名为a的非静态属性 wrappedValue.

@propertyWrapper
struct Email {
    var value: Value?

    var wrappedValue: Value? {
        get {
            返回validate(email: value) ? value : nil
        }
        set {
            value = newValue
        }
    }
    
    private function validate(email: Value?) -> Bool {
        守卫让email = email else{返回false}
        let emailRegEx = "[A-Z0-9a-z .._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        let emailPred = nspreate (format:"SELF MATCHES %@", emailRegEx)
        return emailPred.evaluate(with: email)
    }
}

要在代码中定义这样的包装属性,我们需要使用新的语法.

@Email
var email: String?

因此,我们用注释@标记属性. 属性类型必须与包装器的“wrappedValue”类型匹配. 现在,你可以像处理普通性质一样处理这个性质.

email = "valid@test.com"
打印(电子邮件)// test@test.com
email = "invalid"
print(email) // nil

很好,现在看起来比以前的方法好多了. 但是我们的包装器实现有一个缺点:它不允许包装值的初始值.

@Email
var email: String? = "valid@test.//编译错误.

要解决这个问题,我们需要向包装器添加以下初始化式:

init(wrappedValue) value: value?) {
    self.value = value
}

And that’s it.

@Email
var email: String? = "valid@test.com"
打印(电子邮件)// test@test.com

@Email
var email: String? = "invalid"
print(email) // nil

包装器的最终代码如下:

@propertyWrapper
struct Email {
    var value: Value?
    init(wrappedValue) value: value?) {
        self.value = value
    }
    var wrappedValue: Value? {
        get {
            返回validate(email: value) ? value : nil
        }
        set {
            value = newValue
        }
    }
    
    private function validate(email: Value?) -> Bool {
        守卫让email = email else{返回false}
        let emailRegEx = "[A-Z0-9a-z .._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        let emailPred = nspreate (format:"SELF MATCHES %@", emailRegEx)
        return emailPred.evaluate(with: email)
    }
}

Configurable Wrappers

让我们再举一个例子. 你正在编写一款游戏,你有一个存储用户分数的属性. 要求这个值大于等于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 {
            返回max(min(value, maxValue), minValue)
        }
        set {
            value = newValue
        }
    }
}

@Scores
var scores: Int = 0

这段代码可以工作,但看起来并不通用. 您不能使用不同的约束(不是0和100)来重用它。. 而且,它只能约束整数值. 最好有一个可配置的包装器,它可以约束任何符合Comparable协议的类型. 为了使包装器可配置,我们需要通过初始化器添加所有配置参数. 如果初始化项包含 wrappedValue 属性(属性的初始值),它必须是第一个参数.

@propertyWrapper
struct Constrained {
    private var range: ClosedRange
    private var value:取值
    init(wrappedValue) value: value, _ range: ClosedRange) {
        self.value = value
        self.range = range
    }
    var wrappedValue:值{
        get {
            返回最大(最小)值,范围.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: "test@test.com")

account.email //封装值(String)
account._email // Wrapper(Email)

我们需要访问包装器本身,以便使用我们添加到其中的附加功能. 例如,我们希望Account结构符合Equatable协议. 如果两个帐户的电子邮件地址相同,则它们相等, 电子邮件地址必须不区分大小写.

扩展账号:Equatable {
    static func ==(lhs: Account, rhs: Account) -> Bool {
	 return lhs.email?.lowercased() == rhs.email?.lowercased()
    }
}

It works, 但这不是最好的解决方案,因为我们必须记住在比较电子邮件时添加一个小写()方法. 更好的方法是让电子邮件结构均衡:

扩展邮箱:Equatable {
    static func ==(lhs: Email, rhs: Email) -> Bool {
	 return lhs.wrappedValue?.lowercased() == rhs.wrappedValue?.lowercased()
    }
}

并比较包装器而不是包装值:

扩展账号:Equatable {
    static func ==(lhs: Account, rhs: Account) -> Bool {
	 return lhs._email == rhs._email
    }
}

Projected Value

The @propertyWrapper 注释提供了另一个语法糖——投影值. 此属性可以是您想要的任何类型. 要访问此属性,需要添加 $ 属性名的前缀. 为了解释它是如何工作的,我们使用Combine框架中的一个示例.

The @Published 属性包装器为属性创建发布者,并将其作为投影值返回.

@Published
var message: String

print(message) //打印包装后的值
$message.sink {print($0)} //订阅发布者

As you can see, 我们使用消息来访问包装的属性, 和一个$消息来访问发布者. 如何向包装器添加投影值? 没什么特别的,申报就行了.

@propertyWrapper
struct Published {
    private let subject = PassthroughSubject()
    var wrappedValue:值{
	didSet {
	    subject.send(wrappedValue)
	}
    }
    var projectedValue: AnyPublisher {
	subject.eraseToAnyPublisher()
    }
}

As noted earlier, the projectedValue 属性可以根据您的需要有任何类型.

Limitations

新的属性包装器的语法看起来不错,但也有一些限制, the main ones being:

  1. 他们不能参与错误处理. 被包装的值是一个属性(而不是一个方法),我们不能将getter或setter标记为 throws. For instance, in our Email 例如,如果用户试图设置无效的电子邮件,则不可能抛出错误. We can return nil or crash the app with a fatalError() 呼叫,这在某些情况下可能是不可接受的.
  2. 不允许对属性应用多个包装器. 例如,最好有一个单独的 @CaseInsensitive 包装并将其与 @Email 包装而不是制作 @Email 包装不区分大小写. 但是这样的结构是被禁止的,并且会导致编译错误.
@CaseInsensitive
@Email
    	var email: String?

作为这种特殊情况的变通方法,我们可以继承 Email wrapper from the CaseInsensitive wrapper. However, 继承也有限制——只有类支持继承, 并且只允许使用一个基类.

Conclusion

@propertyWrapper 注释简化了属性包装器的语法, 我们可以用与普通属性相同的方式来操作包装属性. 这使得您的代码,作为一个 Swift Developer 更简洁易懂. 同时,它也有一些我们必须考虑到的限制. 我希望在未来的Swift版本中能够修正其中的一些错误.

如果你想了解更多Swift属性,点击这里 the official docs.

了解基本知识

  • Swift中的属性包装器是什么?

    属性包装器是一种通用结构,它封装对属性的读写访问,并向其添加附加行为.

  • 为什么我们需要属性包装器?

    如果需要约束可用的属性值,则使用属性包装器, 更改读/写访问(如使用数据库或其他存储), 或者添加一些额外的方法,比如值验证.

  • 哪个版本的Swift包含了@propertyWrapper注释?

    @propertyWrapper注释在Swift 5中可用.1 or later.

  • 包装器有什么限制啊?

    他们不能参与错误处理, 并且不允许对属性应用多个包装器.

聘请Toptal这方面的专家.
Hire Now
Aleksandr Gaidukov的头像
Aleksandr Gaidukov

Located in Phuket, Thailand

Member since August 31, 2016

About the author

Alexander有超过9年的应用程序开发经验,并有超过5年的iOS平台开发经验,包括iPhone和iPad.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

Accenture

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.