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],有两个要求:

  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等都在使用它。 

字符串首字母大写

@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中添加了更多的语法糖,这使它更难以理解,并为新来者增加了进入门槛。

posted on 2022-06-01 19:05  梁飞宇  阅读(647)  评论(0)    收藏  举报