Swift PropertyWrapper
属性包装器在管理属性存储方式的代码和定义属性的代码之间添加了一层分离。例如,如果您有提供线程安全检查或将其基础数据存储在数据库中的属性,则必须在每个属性上编写该代码。当您使用属性包装时,您在定义包装时编写一次管理代码,然后通过将其应用于多个属性来重用该管理代码。
简单讲,Property Wrapper 是对属性的一层封装,隐藏与属性相关的逻辑细节,提高代码的复用性。
因为它将属性包装器和属性本身分离开来,并将某些常见的行为抽象出来,使代码更加简洁、可读、易于维护。
1. 定义属性包装器
kotlin
@propertyWrapper // 告诉编译器这是一个属性包装器
struct NumberWrapper {
// 这是包装的属性
var wrappedValue: Int
}
struct Number {
// 告诉编译器使用Wrapper包装器包装该属性
@NumberWrapper var number: Int
}
如何理解 其作用是将属性的 定义代码
与属性的存储方式代码
进行分离 ?
需求要求: 存储的number不可以大于10.
kotlin
@propertyWrapper
struct NumberWrapper {
private var value: Int = 0
var wrappedValue: Int {
get {
return value
}
set {
value = min(newValue, 10)
}
}
}
将属性包装器改成这样的实现,被修饰的number属性,就具有了永不大于10的逻辑。
再来看一个例子:
这是一个姓氏拾取器,通过传入的人名,拾取第一个字作为姓氏。
dart
@propertyWrapper
struct SurnameExtractor {
private var surname: String = ""
var wrappedValue: String {
get { return surname }
set {
surname = String(newValue.prefix(1))
}
}
}
struct XiaoMing {
@SurnameExtractor var surname: String
}
var xiaoming = XiaoMing()
xiaoming.surname = "李小明"
print(xiaoming.surname)
// 输出:李
总结
- 定义属性包装器,需要使用 @propertyWrapper 关键词声明。
- 属性包装器必须要有 wrappedValue 属性(名字是固定的)。
- 属性包装器最好是Struct类型。Struct是值类型,会自动管理饮用,不用担心循环引用。
2. 包装器投影值 projectedValue
projectedValue 为 property wrapper 提供了额外的功能(如:标志某个状态,或者记录 property wrapper 内部的变化等)
在 NumberWrapper 包装器中,我们想知道改属性是否被修正过。就需要主角 projectedValue 登场。
swift
@propertyWrapper
struct NumberWrapper {
private var value: Int = 0
var projectedValue: Bool = false
var wrappedValue: Int {
get {
return value
}
set {
if newValue > 10 {
value = 10
projectedValue = true
} else {
value = newValue
projectedValue = false
}
}
}
init() { }
}
struct SmallNumber {
@NumberWrapper var number: Int
}
var smallNumber = SmallNumber()
smallNumber.number = 5
print(smallNumber.number)
print(smallNumber.$number)
// 输出:5 false
smallNumber.number = 20
print(smallNumber.number)
print(smallNumber.$number)
// 输出:10 true
我们通过属性包装器成功的从名字中获取到了姓氏,在此基础上在实现获取姓氏的中文拼音首字母用来排序,应该怎么处理呢?
swift
@propertyWrapper
struct SurnameExtractor {
private var surname: String = ""
var projectedValue: String {
// 获取中文拼音首字母的方法
return surname.transformToPinyinHead()
}
var wrappedValue: String {
get { return surname }
set {
surname = String(newValue.prefix(1))
}
}
init() {
surname = ""
}
}
var xiaoming = XiaoMing()
xiaoming.surname = "李小明"
print(xiaoming.surname)
print(xiaoming.$surname) // ⚠️注意这里的 $
// 输出:李,L
再来看第三个案例:通过projectedValue直接返回self,为propertyWrapper 提供辅助能力。
less
@propertyWrapper
struct RGBValue {
private var value: Int = 0
var projectedValue: RGBValue { self }
var wrappedValue: Int {
get { return value }
set { value = max(0, min(255, newValue)) }
}
var hex: String {
String(format:"%02X", value)
}
}
struct RGB {
@RGBValue var r: Int
@RGBValue var g: Int
@RGBValue var b: Int
func hexRGB() -> String {
let rHex = $r.hex
let gHex = $g.hex
let bHex = $b.hex
return "#(rHex)(gHex)(bHex)"
}
}
总结
- projectedValue 可以是任意类型
- projectedValue可是存储属性,也可以是计算属性
-
两者都是通过实例的属性名进行访问的。不同的是 projectedValue 需要在属性名前加上 $ 才可以访问。
- wrappedValue: 实例.属性名,属性包装存储的值。
- projectedValue: 实例.$属性名,映射值。
-
projectedValue的命名是固定的。
3. 给属性包装器设置初始值
声明一个这样的属性包装器
swift
@propertyWrapper
struct TenOrLess {
private var maximum: Int
private var number: Int
var wrappedValue: Int {
get { return number }
set { number = min(newValue, maximum) }
}
init() {
maximum = 10
number = 0
}
init(wrappedValue: Int) {
print("init(wrappedValue:)")
maximum = 12
number = min(wrappedValue, maximum)
}
init(wrappedValue: Int, maximum: Int) {
print("init(wrappedValue:maximum:)")
self.maximum = maximum
number = min(wrappedValue, maximum)
}
}
情况1:使用了 @TenOrLess
但没有指定初始化值
less
struct Rectangle {
@TenOrLess var height: Int
@TenOrLess var width: Int
}
var rectangle = Rectangle()
print(rectangle.height, rectangle.width)
// 输出 0 0
情况2:使用了 @TenOrLess
,并指定初始化值
less
struct Rectangle {
@TenOrLess var height: Int = 8
@TenOrLess var width: Int = 8
}
var rectangle = Rectangle()
print(rectangle.height, rectangle.width)
// 输出 8 8
会调用 init(wrappedValue:)
方法 ,print出来 init(wrappedValue:)
.
情况3:使用@TenOrLess,并传参进行初始化
less
struct Rectangle {
@TenOrLess(wrappedValue: 10, maximum: 10) var height: Int
@TenOrLess(wrappedValue: 10, maximum: 10) var width: Int
}
var rectangle = Rectangle()
print(rectangle.height, rectangle.width)
// 输出 10 10
会调用 init(wrappedValue:maximum:)
方法 ,print出来init(wrappedValue:maximum:)
.
less
struct HasWrapperWithInitialValue {
@Wrapper var x = 10
@Wrapper(wrappedValue: 20) var y
}
以上两种声明之间有区别:
- 设置了默认值,会自动调用
init(wrappedValue: Int)
方法,编译器隐式地调用 `init(wrappedValue:) 用0初始化x。 - 初始化方法被明确指定为属性的一部分。
情况4:使用@TenOrLess,并传参进行初始化,并给属性默认值
less
struct Rectangle {
@TenOrLess(wrappedValue: 10, maximum: 10) var height: Int = 0
@TenOrLess(wrappedValue: 10, maximum: 10) var width: Int = 0
}
❌: Extra argument 'wrappedValue' in call。 在调用中有多余的参数。
4. 访问属性包装器
swift
@propertyWrapper
struct VisitWrapper<T> {
var wrappedValue: T
var projectedValue: VisitWrapper<T> { return self }
func welcome() {
print("welcome ~~")
}
}
struct VisitHasWrapper {
@VisitWrapper var x = 0
func welcome() {
/// 这里的_x是包装器的实例,因此可以调用welcome方法,但是从VisitHasWrapper外部调用就会产生便衣错误 "'_x' is inaccessible due to 'private' protection level"
_x.welcome()
// $符号是访问包装器属性的一个语法糖
$x.welcome()
print(x) // 访问的wrappedValue, 输出:0
print(_x) // 访问的是 wrapper type itself, VisitWrapper<Int>(wrappedValue: 0)
print($x) // 访问的是projectedValue, VisitWrapper<Int>(wrappedValue: 0)
}
}
- $符号是访问包装器属性的一个语法糖
- x: 访问的wrappedValue
- _ x: 访问的是 wrapper type itself
- $x: 访问的是projectedValue
5. 使用限制
5. 1 协议的属性不支持使用属性包装器
❌: property 'some' declared inside a protocol cannot have a wrapper。在协议中声明的属性'some'不能有包装器
swift
protocol SomeProtocol {
@TenOrLess var some: Int { get set }
}
5.2 extension中不可以使用
❌:Non-static property 'some' declared inside an extension cannot have a wrapper. 在扩展内声明的非静态属性'some'不能有包装器.
kotlin
extension Rectangle {
@TenOrLess var some: Int { return 5 }
}
5.3 enum中不可以使用
❌:Property wrapper attribute 'TenOrLess' can only be applied to a property. 属性包装器属性'TenOrLess'只能应用于属性
sql
enum SomeEnum: Int {
@TenOrLess case one
case two
}
5.4 class里的 wrapper property
不能重写
❌:Cannot override with a stored property 'some'. 不能用存储属性"some"重写
kotlin
class SomeClass {
@TenOrLess var some: Int
}
class OtherClass: SomeClass {
override var some: Int = 0
}
5.5 wrapper 不能定义 getter
或 setter
方法
❌:Property wrapper cannot be applied to a computed property. 属性包装器不能应用于计算属性
kotlin
struct SomeStruct {
@TenOrLess var some: Int {
return 0
}
}
5.6 wrapper
属性不能被 lazy
、 @NSCopying
、 @NSManaged
、 weak
、 或者 unowned
修饰
6. 其他的一些示例
6.1 验证传入的字符串值是否为空。
swift
@propertyWrapper
struct CheckEmptyString {
private var value: String = ""
var wrappedValue: String {
get { value }
set {
if newValue.isEmpty {
assert(false, "传入的值为空")
self.value = "unknowed"
} else {
self.value = newValue
}
}
}
}
struct Person {
@CheckEmptyString var name: String
}
var p1 = Person()
p1.name = ""
print(p1.name)
6.2 给UserDefault提供一个便利的调用方法。
less
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
enum GlobalSettings {
@UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
static var isFooFeatureEnabled: Bool
@UserDefault(key: "BAR_FEATURE_ENABLED", defaultValue: false)
static var isBarFeatureEnabled: Bool
}
GlobalSettings.isFooFeatureEnabled = true
let value = GlobalSettings.isFooFeatureEnabled