如何编写 swift 属性 包装器?

How to compose swift property wrappers?

我最近一直在试验 swift 属性 包装器,想知道是否有任何方法可以将它们组合在一起以实现更加模块化的体系结构。例如:

@WrapperOne @WrapperTwo var foo: T

查看文档一无所获。关于如何执行此操作的唯一参考是 这个GitHub页面(https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md)(下面的引号)似乎说这是可能的。其他文章说它们很难编写,但没有解释如何去做。但是,我无法弄清它的正反面,如果有人可以向我展示一些有关如何实现它的示例代码,我将不胜感激(参见 post 的底部)。

When multiple property wrappers are provided for a given property, the wrappers are composed together to get both effects. For example, consider the composition of DelayedMutable and Copying:

@DelayedMutable @Copying var path: UIBezierPath

Here, we have a property for which we can delay initialization until later. When we do set a value, it will be copied via NSCopying's copy method. Composition is implemented by nesting later wrapper types inside earlier wrapper types, where the innermost nested type is the original property's type. For the example above, the backing storage will be of type DelayedMutable<Copying<UIBezierPath>> and the synthesized getter/setter for path will look through both levels of .wrappedValue:

private var _path: DelayedMutable<Copying<UIBezierPath>> = .init()
var path: UIBezierPath {
    get { return _path.wrappedValue.wrappedValue }
    set { _path.wrappedValue.wrappedValue = newValue }
}

Note that this design means that property wrapper composition is not commutative, because the order of the attributes affects how the nesting is performed: @DelayedMutable @Copying var path1: UIBezierPath // _path1 has type DelayedMutable> @Copying @DelayedMutable var path2: UIBezierPath // error: _path2 has ill-formed type Copying> In this case, the type checker prevents the second ordering, because DelayedMutable does not conform to the NSCopying protocol. This won't always be the case: some semantically-bad compositions won't necessarily by caught by the type system. Alternatives to this approach to composition are presented in "Alternatives considered."

理想情况下,我想实现如下内容:

@propertyWrapper
struct Doubled {
    var number: Int
    var wrappedValue: Int {
        get { (value * 2) }
        set { value = Int(newValue / 2) }
    }
    init(wrappedValue: Int) {
        self.number = wrappedValue
    }
}

@propertyWrapper
struct Tripled {
    var number: Int
    var wrappedValue: Int {
        get { (value * 3) }
        set { value = Int(newValue / 3) }
    }
    init(wrappedValue: Int) {
        self.number = wrappedValue
    }
}

这样才能实现:

@Tripled @Doubled var number = 5

我知道这个例子是实现 属性 包装器组合的一个有点愚蠢的理由,但这仅仅是为了学习新功能时的简单性。

如有任何帮助,我们将不胜感激。

使用多个 属性 包装器存在一些问题,因此部分功能已从 Swift 5.1 中删除,但它将在 5.2 中可用。在那之前你不能像这样直接使用多个 属性 包装器。

从 Swift 5.2 开始,嵌套的 属性 包装器变得更加稳定,但使用它们仍然有点困难。我写过一篇文章here,但是诀窍是因为外包装的wrappedValue是内包装的类型,而内包装的wrappedValue是直接的属性 类型,你必须让包装器在两种类型上运行。

我遵循的基本思想是创建一个包装器运行的协议。然后,您也可以让其他包装器也符合该协议,以启用嵌套。

例如,在Doubled的情况下:

protocol Doublable {
    func doubling() -> Self
    func halving() -> Self
}

@propertyWrapper
struct Doubled<T: Doublable> {
    var number: T
    var wrappedValue: T {
        get { number.doubling() }
        set { number = newValue.halving() }
    }
    init(wrappedValue: T) {
        self.number = wrappedValue
    }
}

extension Int: Doublable {
    func doubling() -> Int {
        return self * 2
    }

    func halving() -> Int {
        return Int(self / 2)
    }
}

extension Doubled: Doublable {
    func doubling() -> Self {
        return Doubled(wrappedValue: self.wrappedValue)
    }

    func halving() -> Self {
        return Doubled(wrappedValue: self.wrappedValue)
    }
}

struct Test {
    @Doubled @Doubled var value: Int = 10
}

var test = Test()
print(test.value) // prints 40

您可以对 Tripled 执行相同的操作,使用 Tripleable 协议,等等。

但是,我应该注意,与其嵌套 @Tripled @Doubled,不如创建另一个像 @Multiple(6) 这样的包装器可能更好:这样你就不必处理任何协议,但你会得到同样的效果。