Swift 属性包装器

167 阅读5分钟

我们来看 The Swift Programming Language (6.2.3) 中的例子。

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

结构 TwelveOrLess 是属性包装器,属性包装器可以是 class、struct 和 enum。属性包装器需要有个属性 wrappedValue,表示被包装的值。TwelveOrLess 的 wrappedValue 属性是计算属性,读写私有的存储属性 number,其 setter 确保 number 小于或等于 12。由于存储属性 number 有默认值,编译器生成 default 初始化器 init()。

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

结构 SmallRectangle 应用包装器 TwelveOrLess 到属性 height 和 width,编译器重写代码为:

struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}

生成 _height 和 _width 存储属性,存储包装器 TwelveOrLess 的实例。height 和 width 成为计算属性,访问 _height 和 _width 的 wrappedValue。

v2-a712b4a4c7d80561227b6bc40f5c8608_1440w.png

属性 height 和 width 未指定初始值(此时也不能指定初始值),编译器使用 init() 初始化器来设置包装器,编译器还会为 SmallRectangle 生成 default 初始化器和 memberwise 初始化器。

init() {
}

init(
    height: TwelveOrLess = TwelveOrLess(),
    width: TwelveOrLess = TwelveOrLess()
)

memberwise 初始化器的参数 height 和 width 为包装器类型 TwelveOrLess。

如果 TwelveOrLess 增加初始化器 init(wrappedValue: Int),

@propertyWrapper
struct TwelveOrLess {   
    private var number = 0
    
    init(wrappedValue: Int) {
        self.number = min(wrappedValue, 12)
    }

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

TwelveOrLess 不会生成 default 初始化器,SmallRectangle 的属性 height 和 width 未指定初始值(此时可以指定初始值),编译器未能使用 init() 初始化器来设置包装器,重写代码为

struct SmallRectangle {
    private var _height: TwelveOrLess
    private var _width: TwelveOrLess

    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }

    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
    
    init(height: Int, width: Int) {
        _height = TwelveOrLess(wrappedValue: height)
        _width = TwelveOrLess(wrappedValue: width)
    }
}

参数 height 和 width 的类型为原始类型 Int。

如果 TwelveOrLess 增加初始化器 init(),

@propertyWrapper
struct TwelveOrLess {   
    private var number = 0
    
    init() {
        number  = 1
    }
    
    init(wrappedValue: Int) {
        self.number = min(wrappedValue, 12)
    }

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

SmallRectangle 的属性 height 和 width 未指定初始值(此时可以指定初始值),编译器使用 init() 初始化器来设置包装器。编译器重写 SmallRectangle 为

struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()

    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }

    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
    
    init() {
    }

    init(height: TwelveOrLess = TwelveOrLess(), width: TwelveOrLess = TwelveOrLess() {
        _height = height
        _width = width
    }
}

memberwise 初始化器的参数 height 和 width 为包装器类型 TwelveOrLess。

由于包装器 TwelveOrLess 有初始化器 init(wrappedValue: Int),SmallRectangle 的属性 height 和 width 可以指定初始值,下面我们指定初始值。

struct SmallRectangle {
    @TwelveOrLess var height: Int = 1
    @TwelveOrLess var width: Int = 1
}

编译器生成的代码为

struct SmallRectangle {
    private var _height = TwelveOrLess(wrappedValue: 1)
    private var _width = TwelveOrLess(wrappedValue: 1)
    
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
    
    init() {
    }
    
    init(height: Int, width: Int) {
        _height = TwelveOrLess(wrappedValue: height)
        self.width = TwelveOrLess(wrappedValue: width)
    }
}

memberwise 初始化器的参数 height 和 width 为原始类型 Int。

总结:对一个 struct 的某个属性应用包装器,以下情况 memberwise 初始化器对应参数类型为原始类型,

  • 属性指定初始值(包装器有初始化器 init(wrappedValue:) 才能指定),像 @TwelveOrLess var height: Int = 1
  • 属性未指定初始值,包装器有初始化器 init(wrappedValue:),并且没有 init()

其它情况,memberwise 初始化器参数类型为包装器类型。

根据上面的结论,我们指定 height 有初始值,width 没有初始值,看看 memberwise 初始化器参数类型。

struct SmallRectangle {
    @TwelveOrLess var height: Int = 1
    @TwelveOrLess var width: Int
}

height 属性指定了初始值,对应的参数类型为原始类型 Int,width 未指定初始值,但是包装器有 init(),因此对应的参数类型为包装器类型 TwelveOrLess,相信大家已经猜到了,

init(height: Int = 1, width: TwelveOrLess = TwelveOrLess()) {
    _height = TwelveOrLess(wrappedValue: height)
    _width = width
}

下面我们把 TwelveOrLess 自定义的 init() 去掉,TwelveOrLess 代码为上面未增加自定义的 init() 之前的代码。width 未指定初始值,包装器有初始化器 init(wrappedValue:),并且没有 init(),因此对应的参数类型为原始类型,因此生成的 memberwise 初始化器为

init(height: Int = 1, width: Int) {
    _height = TwelveOrLess(wrappedValue: height)
    _width = TwelveOrLess(wrappedValue: width)
}

上面的访问级别都是默认的 internal,下面我们看看属性的访问级别为 private 对于 memberwise 初始化器的生成有什么影响。 我们把属性 width 改为 private,

struct SmallRectangle {
    @TwelveOrLess var height: Int = 1
    @TwelveOrLess private var width: Int
}

生成的初始化器仍为 init(height: Int = 1, width: Int),但是由于 width 属性是 private,此初始化器的访问级别也是 private,在 SmallRectangle 所在的文件调用它的此初始化器会报错,'SmallRectangle' initializer is inaccessible due to 'private' protection level。

下面我们只把属性 height 改为 private,

struct SmallRectangle {
    @TwelveOrLess private var height: Int = 1
    @TwelveOrLess var width: Int
}

此时生成的初始化器为

init(width: Int) {
    _width = TwelveOrLess(wrappedValue: width)
}

排除了 private 并且指定初始值的 height。

总结:private 并且指定了初始值的属性,生成的初始化器中不包含相应的参数。

除了被包装的值,属性包装器可以通过定义一个 projected value 暴露额外的功能。

@propertyWrapper
struct SmallNumber {
    private var number: Int
    private(set) var projectedValue: Bool
    
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }
    
    init() {
        self.number = 0
        self.projectedValue = false
    }
}

上面代码中,SmallNumber 结构增加了一个属性 projectedValue,用来记录包装器是否调整了被包装的值。

struct SomeStructure {
    @SmallNumber var someNumber: Int
}

var someStructure = SomeStructure()
print(someStructure.$someNumber)
// 打印 false
someStructure.someNumber = 55
print(someStructure.$someNumber)
// 打印 true

通过在被包装的属性名前增加 $ 来访问包装器的 projectedValue。