如何编写Swift属性包装器?

20

我最近在尝试使用Swift属性包装器,并想知道是否有任何方法可以将它们组合在一起以实现更模块化的架构。例如:

@WrapperOne @WrapperTwo var foo: T

在文档中什么也找不到。唯一有关如何做到这一点的参考资料是在此 GitHub 页面上(https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md)(下面引用),似乎表明这是可能的。其他文章称它们很难组合,但没有解释如何进行操作。然而,我无法理解其中内容,如果有人能向我展示一些实现此功能的示例代码(请参见本文末尾),我将不胜感激。

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

我知道这个例子有点儿荒谬,但只是为了在学习新功能时更简单明了地表达。

如有需要,我们将乐意提供帮助。


你可以看一下这个示例代码,类似于这样的Pod,链接在https://github.com/muhammadali2012/Model上。 - undefined
3个回答

9
从Swift 5.2开始,嵌套属性包装器变得更加稳定了,但仍然有些难以使用。我在这篇文章中写了一些相关内容here,但关键在于由于外部包装器的wrappedValue是内部包装器的类型,而内部包装器的wrappedValue是直接的属性类型,您必须使包装器对两种类型都起作用。
我所遵循的基本思想是创建一个协议,让包装器操作它。然后,您还可以使其他包装器符合该协议,以实现嵌套。
例如,在Double的情况下:
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)可能更好:这样你就不必处理任何协议,但你将得到相同的效果。


1
谢谢您。这真的很有用! - Ben
这是我读过的最神奇的代码片段!有这么多的意义上下文。Int 可以与 Doubled 不匹配,后者是一个属性包装器,用于加倍逻辑和缓冲多个注释的结果,只为打印出双倍的十等于四十。太棒了 Noah! - lyzkov

3

使用多个属性包装器存在一些问题,因此该功能的一部分被从Swift 5.1中删除了,但将在5.2中提供。在那之前,您不能像这样直接使用多个属性包装器。


1
谢谢您的回复。我会记得关注5.2版本!不知道它何时发布? - Ben
5.2刚开始在swift.org上提供工具链,所以可能需要一段时间,我猜大约在WWDC左右。如果您不打算在一段时间内部署此代码,并且可以接受开发工具链带来的错误,那么可以使用它来测试或开发具有多个属性包装器的内容。 - bscothern
第一个测试版刚刚发布!https://developer.apple.com/documentation/xcode_release_notes/xcode_11_4_beta_release_notes - Josh

0

最简单的解释。

  1. 属性包装器在属性上从左到右应用,并将以相同的模式初始化。
  2. 属性包装器的包装值类型必须与其应用的类型相同。

有了这个想法,让我们考虑两个包装器

@propertyWrapper struct TwoTimer {
var wrappedValue : Int
var projectedValue : Int {
    return 2 * wrappedValue
}}

@propertyWrapper struct TenTimer {
var wrappedValue : Int
var projectedValue : Int {
    return 10 * wrappedValue
}}

在最简单的形式下,使用 $ 访问的预计值将返回包装后的值十倍或两倍

现在让我们尝试实现

@TenTimer var tenTimevalue = 4
@TwoTimer var twoTimevalue = 4
@TwoTimer @TenTimer var value = 4

第三行是我们最感兴趣的,因为我们希望我们的值被十倍和两倍,但编译器会说

无法将类型 'TenTimer' 的值转换为指定类型 'Int'

因为 TwoTimer 的 wrappedvalue 左侧包装器(记住 TwoTimer 是左侧的)是 Int,我们可以简单地将其更改为 TenTimer 类型,它就可以工作了,即

@propertyWrapper struct TwoTimer {
var wrappedValue : TenTimer
var projectedValue : Int {
    return 2 * wrappedValue.wrappedValue
}}

这将消除编译器错误,但会使TwoTimer仅适用于TenTimer类型,并导致非常紧密的耦合,没有可重用性,属性包装器的目的立即消失。要使TwoTimer适用于任何类型,我们必须使用协议和泛型。

让我们创建一个具有以下要求的协议MainType

protocol MainType  {
associatedtype  T
var wrappedValue : T { get set }

}

现在让我们确认一下这个类型在我们两个包装器中的情况

@propertyWrapper struct TenTimer : MainType  {
var wrappedValue : Int
var projectedValue : Int {
    return 10 * wrappedValue
}

}

@propertyWrapper struct TwoTimer : MainType {
var wrappedValue : Int
var projectedValue : Int {
    return 2 * wrappedValue
}

}

现在,TwoTimer可以使用MainType而不是TenTimerOnly进行工作,因此让我们也更改该类型,使用MainType的通用类型替换TwoTimer中的具体TenTimer类型,以使TwoTimer能够与MainType一起工作,而不仅仅是TenTimerOnly。

@propertyWrapper struct TwoTimer<T : MainType> : MainType {
var wrappedValue : T
var projectedValue : Int {
    return 2 * wrappedValue
}

}

已经接近成功,但编译器无法将泛型类型T(主要类型)与Int相乘并引发错误

无法将类型“T”的值转换为预期参数类型“Int”

将T强制转换为Int也无法解决问题,因此

让我们创建一个带有value函数的IntNumericType类型

protocol IntNumericType {
func value() -> Int

}

在 TenTimer 中确认

@propertyWrapper struct TenTimer : MainType, IntNumericType  {
var wrappedValue : Int
var projectedValue : Int {
    return 10 * wrappedValue
}
func value() -> Int {
    return wrappedValue
}

}

将通用的 where 子句应用于 Twotimer 通用值,并在 TwoTimer 项目 ID 值中使用它。
@propertyWrapper struct TwoTimer<T : MainType> : MainType where T : IntNumericType {
var wrappedValue : T
var projectedValue : Int {
    return 2 * wrappedValue.value()
}

}

这就是我们可以在任何符合MainType协议的值上使用Twotimer的方法。

注意:我们还可以通过扩展将Tentimer转换为确切实现的Towtimer,以便更好地重用。

我们也可以使用苹果数字协议来提高可用性,而不是使用我们自己的IntNumericType。

例子一开始可能看起来不适合使用泛型:P


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接