阅读 3387

Swift 5 属性包装器Property Wrappers完整指南

Property wrapper是Swift语言的新特性,它使我们能够自定义类型并在各处使用,该类型实现getset方法的功能。 在本文中,我们将研究有关属性包装器(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],有两个要求:

  1. 必须使用属性@propertyWrapper进行定义。
  2. 它必须具有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
}
复制代码

以上两种声明之间有区别:

  1. 编译器隐式地调用init(wrappedValue:)0初始化x

  2. 初始化方法被明确指定为属性的一部分。

访问属性包装器

在属性包装器中提供额外的行为通常很有用:

@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@NSManagedweakunowned
  • 具有包装器的属性不能具有自定义的setget方法。
  • wrappedValueinit(wrappedValue :)projectedValue必须具有与包装类型本身相同的访问控制级别
  • 不能在协议或扩展中声明带有包装器的属性。

使用例子

当属性包装程序真正发挥作用时,它们具有许多使用场景。 内置于SwiftUI框架中的: @State, @Published, @ObservedObject, @EnvironmentObject and @Environment等都在使用它。 其他的已在Swift社区中广泛使用,例如:

总结

属性包装器是Swift 5的一项强大功能,它在管理属性存储方式与定义属性的代码之间增加了一层封装:

在决定使用属性包装器时,请确保考虑到它们的缺点:

  • 属性包装器具有多种语言限制,如上面【使用限制】一节中所说的。
  • 属性包装器需要Swift 5.1,Xcode 11和iOS 13。
  • 属性包装器在Swift中添加了更多的语法糖,这使它更难以理解,并为新来者增加了进入门槛。

原文:Vadim Bulavin

翻译整理:乐Coding

参考:

【1】:github.com/apple/swift…

【3】:docs.swift.org/swift-book/…

【2】:www.jianshu.com/p/ff4c048f0…

logo

文章分类
iOS
文章标签