Property wrapper是Swift语言的新特性,它使我们能够自定义类型并在各处使用,该类型实现get
和set
方法的功能。 在本文中,我们将研究有关属性包装器(Property wrapper
)的所有内容:
- 他们解决了什么问题?
- 如何实现属性包装器?
- 如何访问属性包装器、包装的值和投影(projection)?
- 如何在代码中使用属性包装器?
- 属性包装器有哪些限制?
理解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 // Prints 'New value is 1'
如果我们继续记录更多这样的属性,那么代码很快就会变得一团糟。 为了不用为每个新属性一遍又一遍地复制相同的代码,我们声明一个新类型,该新类型将执行日志记录:
struct ConsoleLogged<Value> {
private var value: Value
init(wrappedValue: Value) {
self.value = wrappedValue
}
var wrappedValue: Value {
get { value }
set {
value = newValue
print("New value is \(newValue)")
}
}
}
这是我们如何使用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使用
对于属性包装器(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
等都在使用它。 其他的已在Swift社区中广泛使用,例如:
- www.vadimbulavin.com/swift-atomi…
- github.com/SvenTiigi/V…
- www.avanderlee.com/swift/prope…
- github.com/marksands/B…
总结
属性包装器是Swift 5的一项强大功能,它在管理属性存储方式与定义属性的代码之间增加了一层封装:
在决定使用属性包装器时,请确保考虑到它们的缺点:
- 属性包装器具有多种语言限制,如上面【使用限制】一节中所说的。
- 属性包装器需要Swift 5.1,Xcode 11和iOS 13。
- 属性包装器在Swift中添加了更多的语法糖,这使它更难以理解,并为新来者增加了进入门槛。
原文:Vadim Bulavin
翻译整理:乐Coding
参考:
