swift小知识之属性包装
理解Property Wrappers
为了更好地了解属性包装器,让我们举一个例子来看一下它们可以解决哪些问题。 假设我们要向我们的app添加一种日志记录功能。 每次属性更改时,我们都会将其新值打印到Xcode控制台。 这样追踪错误或追踪数据流时非常有用。 实现此目的的直接方法是覆盖setter
:
struct Bar { private var _x = 0 var x:Int { get{_x} set{ _x = newValue print("New Value is \(newValue)") } } } var bar = Bar() bar.x = 1
如果我们继续记录更多这样的属性,那么代码很快就会变得一团糟。 为了不用为每个新属性一遍又一遍地复制相同的代码,我们声明一个新类型,该新类型将执行日志记录:
struct ConsoleLogged<Value> { private var value: Value var wrappedValue: Value { get { value } set { value = newValue print("New value is \(newValue)") } } init(wrappedValue: Value) { self.value = wrappedValue } }
这是我们如何使用ConsoleLogged
重写Bar
的方法:
struct Bar { private var _x = ConsoleLogged<Int>(wrappedValue: 0) var x: Int { get { _x.wrappedValue } set { _x.wrappedValue = newValue } } } var bar = Bar() bar.x = 1 // Prints 'New value is 1'
Swift为此模式提供了语言上的支持。 我们需要做的就是将@propertyWrapper
属性添加到我们的ConsoleLogged
类型中:
@propertyWrapper struct ConsoleLogged<Value> { // 其他代码没变,这里不粘贴了。 }
您可以将property wrapper视为常规属性,它将get和set方法委托给其他类型。
在属性声明的地方,我们可以指定哪个包装器实现它:
struct Bar { @ConsoleLogged var x = 0 } var bar = Bar() bar.x = 1 // Prints 'New value is 1'
属性@ConsoleLogged是一个语法糖,它会转换为我们代码的先前版本。
译者注:通过@propertyWrapper可以移除掉一些重复或者类似的代码。
Property Wrapper使用
我们通过 @propertyWrapper
来标识structure, enumeration, or class
来实现属性包装
对于属性包装器(Property Wrapper)类型[1],有两个要求:
- 必须使用属性
@propertyWrapper
进行定义。 - 它必须具有
wrappedValue
属性。
下面就是最简单的包装器的“杨紫”:
@propertyWrapper struct Wrapper<T> { var wrappedValue: T }
现在,我们可以使用@Wrapper:
struct HasWrapper { @Wrapper var x: Int } let a = HasWrapper(x: 0)
我们可以通过两种方式将默认值传递给包装器:
struct HasWrapperWithInitialValue { @Wrapper var x = 0 // 1 @Wrapper(wrappedValue: 0) var y // 2 }
以上两种声明之间有区别:
-
编译器隐式地调用
init(wrappedValue:)
用0
初始化x
。 -
初始化方法被明确指定为属性的一部分。
访问属性包装器
在属性包装器中提供额外的行为通常很有用:
@propertyWrapper struct Wrapper<T> { var wrappedValue: T func foo() { print("Foo") } }
我们可以通过在变量名称上添加下划线来访问包装器类型:
struct HasWrapper { @Wrapper var x = 0 func foo() { _x.foo() } }
这里的_x是包装器的实例,因此我们可以调用foo()。 但是,从HasWrapper
的外部调用它会产生编译错误:
let a = HasWrapper() a._x.foo() // ❌ '_x' is inaccessible due to 'private' protection level
原因是合成包装器默认具有private
访问级别。 我们可以使用projection
来解决这一问题。
通过定义
projectedValue
属性,属性包装器可以公开更多API。 对projectedValue
的类型没有任何限制。
@propertyWrapper struct Wrapper<T> { var wrappedValue: T var projectedValue: Wrapper<T> { return self } func foo() { print("Foo") } }
$
符号是访问包装器属性的一个语法糖:
let a = HasWrapper() a.$x.foo() // Prints 'Foo'
总之,有三种访问包装器的方法:
struct HasWrapper { @Wrapper var x = 0 func foo() { print(x) // `wrappedValue` print(_x) // wrapper type itself print($x) // `projectedValue` } }
使用限制
Property wrappers并非没有限制。 他们强加了许多限制:
- 带有包装器的属性不能在子类中覆盖。
- 具有包装器的属性不能是
lazy
,@NSCopying
,@NSManaged
,weak
或unowned
。 - 具有包装器的属性不能具有自定义的
set
或get
方法。 wrappedValue
,init(wrappedValue :)
和projectedValue
必须具有与包装类型本身相同的访问控制级别- 不能在协议或扩展中声明带有包装器的属性。
使用例子
当属性包装程序真正发挥作用时,它们具有许多使用场景。 内置于SwiftUI框架中的: @State
, @Published
, @ObservedObject
, @EnvironmentObject
and @Environment
等都在使用它。
字符串首字母大写
@propertyWrapper struct Capitalized { var wrappedValue: String { didSet { wrappedValue = wrappedValue.capitalized } } init(wrappedValue: String) { self.wrappedValue = wrappedValue.capitalized } }
定义User
,并使用Capitalized
struct User { @Capitalized var firstName: String @Capitalized var lastName: String } let user = User(firstName: "jack", lastName: "long") print(user.firstName, user.lastName) // Jack Long
属性加锁使用
@propertyWrapper class LockAtomic<T> { private var value: T private let lock = NSLock() public init(wrappedValue value: T) { self.value = value } public var wrappedValue: T { get { getValue() } set { setValue(newValue: newValue) } } // 加锁处理获取数据 func getValue() -> T { lock.lock() defer { lock.unlock() } return value } // 设置数据加锁 func setValue(newValue: T) { lock.lock() defer { lock.unlock() } value = newValue } }
使用LockAtomic
@LockAtomic var json: [String: String]? json = ["a": "1"] print(json) // Optional(["a": "1"])
总结
属性包装器是Swift 5的一项强大功能,它在管理属性存储方式与定义属性的代码之间增加了一层封装:
在决定使用属性包装器时,请确保考虑到它们的缺点:
- 属性包装器具有多种语言限制,如上面【使用限制】中所说的。
- 属性包装器需要Swift 5.1,Xcode 11和iOS 13。
- 属性包装器在Swift中添加了更多的语法糖,这使它更难以理解,并为新来者增加了进入门槛。