Swift | 属性包装器

Posted 忧郁的思想家

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift | 属性包装器相关的知识,希望对你有一定的参考价值。

Property Wrapper将常规属性的get和set方法委托给其他类型,或者使用其它类型存储属性的值,通过$属性名拓展一些行为;该类型使用@propertyWrapper声明,可以是枚举,结构体,类,抽取成类型,减少样板代码,并且可以定义更多的功能!


  1. 为什么会有属性包装器

  2. 如何实现一个属性包装器

  3. 属性包装器的限制

  4. 属性包装器的应用



1.为什么会有属性包装器


当我们想要记录某个属性的值,或者实现懒加载属性时,我们可能会这样(这里不使用lazy关键字)

struct Foo { // lazy var foo = 1738 private var _foo:Int?  var foo:Int{ mutating get{ if let value = _foo { return value } let initialValue = 1738 _foo = initialValue return initialValue } set{ _foo = newValue } }}

看起来还算完美,但是如果想实现多个时,你会发现重复的代码到处都是,显然这一点都不优雅~,swift5.1的时候有了Property Wrapper,完全可以解决这个问题,其实像SwiftUI中的@State,@Bindling,@ObjectBinding,@EnvironmentObject就是官方实现的Property Wrapper类型.


2.如何实现一个属性包装器


那么我们现在使用Property Wrapper实现上面Lazy的功能.

@propertyWrapperenum Lazy<Value> {   case uninitialized( () -> Value ) case initialized(Value)  var wrappedValue:Value{ mutating get{ switch self { case .uninitialized(let initializer): let value = initializer() self = .initialized(value) return value case .initialized(let value): return value } } }  init(wrappedValue:@autoclosure @escaping () -> Value) { self = .uninitialized(wrappedValue) }}struct Foo { @Lazy var foo = 1738}

实现一个属性包装器类型可以是enum,struct,或者class,类型前必须声明@propertyWrapper,告诉编译器这是一个属性包装器,验证相应规则.

而且必须有一个wrappedValue的实例属性,编译器通过访问wrappedValue来包装值.


@Lazy var foo = 1738//等同于private var _foo:Lazy<Int> = Lazy<Int>(wrappedValue: 1738)var foo:Int{ get { return _foo.wrappedValue} set { _foo.wrappedValue = newValue}}

使用属性包装器修饰一个常规属性,实际上会生成这个属性包装器对象,通过设置初始值(init(),init(wrappedValue:)),或通过属性包装器的自定义构造方法来生成,我们可以在类型中使用_实例属性来获取这个属性包装器,但是在外部是无法获取的.


swift也给我们提供了在外部使用的方案,那就是在属性包装器类型中实现projectedValue这个属性,返回我们想要让外部获取的内容,在外部我们可以使用$属性来获取.


projectedValue1.通过$属性来访问,避免冲突2.添加附加功能3.可以是另外一种类型而不是属性本身4.可以是self,暴露属性包装器
//若实现了projectedValue@Lazy var foo = 1738//等同于private var _foo:Lazy<Int> = Lazy<Int>(wrappedValue: 1738)public var foo:Int{ get { return _foo.wrappedValue} set { _foo.wrappedValue = newValue}}public var $foo:Int{ get { return _foo.projectedValue} set { _foo.projectedValue = newValue }}


总结一下:

1. @propertyWrapper,init(initialValue:),wrappedValue 和 projectedValue 构成了一个propertyWrapper最重要的部分,其中@propertyWrapper和wrappedValue是必须的.

2. 属性包装器的实现原理是将属性的get,set方法委托给其它类型,所有该属性不可以有get,set,但是可以有willSet,didSet.

3.属性包装器类型要有合适的访问级别.


这里介绍属性包装器初始化的规则

@Lazy var foo:Int //当前类型没有提供初始化方法并且属性包装器类型提供了无参数的初始化方法(init())时,将调用包装器类型的init()来初始化存储的属性。
@Lazy var foo:Int = 10 //将会调用init(wrappedValue:)初始化存储的属性
@Converter(initialValue: "100", from: "USD", to: "CNY", rate: 6.88) var usd_cny //自定义属性包装器类型的初始化方法


3.属性包装器的限制


- 属性必须是var声明,且不能是throws。

- 带有包装器的属性不能在子类中覆盖。

- 具有包装器的属性不能是lazy@NSCopying@NSManagedweakunowned

- 具有包装器的属性不能具有自定义的setget方法。

- wrappedValueinit(wrappedValue :)projectedValue必须具有与包装类型本身相同的访问控制级别

- 不能在协议或扩展中声明带有包装器的属性。

- 不能使用属性包装器来注释集合中的键或值类型 //var headers: [@CaseInsensitive String: String] // ❌

 - 属性包装器的组合不是交换操作,声明它们的顺序会影响它们的行为。最外面的包装器作用于最里面的包装器类型的值.

能使用属性包装器来定义一个新类型,并限制可能的值。

 typealias pH = @Clamping(0...14) Double // ❌

 func acidity(of: Chemical) -> pH {}

等等...感觉好复杂....


4. 属性包装器的应用


什么时候使用属性包装器呢?我们可以结合其优点来展示

1.将属性存储与属性定义分离2.约束值3.属性赋值时转换数据4.减少样板代码5.添加缺少的功能而不更改(扩展)类型6.影响特定实例而不是所有实例7.作为数据访问的中间人

下面完整实现一个在指定时间外失效的功能,比如token值,或者支付时的订单签名...

@propertyWrapperstruct Expirable<Value> {  var projectedValue:Expirable{ return self }  var wrappedValue : Value?{ get{ isValid ? storage?.value : nil } set{ storage = newValue.map{ newValue in let expirationDate = Date().addingTimeInterval(duration) return (newValue,expirationDate) } } } let duration : TimeInterval private var storage:(value:Value,expirationDate:Date)?  //是否有效 var isValid:Bool { guard let storage = storage else{ return false } return storage.expirationDate >= Date() } init(duration:TimeInterval) { self.duration = duration }}



参考:

https://nshipster.com/propertywrapper/

https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md

https://juejin.im/post/5df05060518825122030809e

以上是关于Swift | 属性包装器的主要内容,如果未能解决你的问题,请参考以下文章

Swift | 属性包装器

Swift 属性包装器可以引用其包装的属性所有者吗?

Swift之深入解析SwiftUI属性包装器如何处理结构体

在 Swift 属性包装器中公开字典

原子属性包装器仅在声明为类而不是结构时才有效

php 一个自定义的try..catch包装器代码片段,用于执行模型函数,使其成为一个单行函数调用