探索 Property wrappers in Swift

Properties 属性

对于属性,Swift 官方文档的释义是:访问存储在实例或类型中的存储和计算值。而属性包装器(Property Wrappers)是Swift 5.1 的新特性之一,它的主要作用是将通用的模板代码封装成一种简洁的表达形式,极大地提高了编码的效率。

Property wrappers

Property Wrappers:属性包装器,Swift 官方文档对其定义是:属性包装器是在管理属性存储的代码和定义属性的代码之间添加一层分离。例如,如果您的属性需要提供线程安全检查或将其基础数据存储在数据库中,您需要在每个属性上编写该代码。当您使用属性包装器时,您只需在定义包装器时编写一次管理代码,然后通过将其应用于多个属性来重用该管理代码。

我理解的是:定义属性时,指定一个属性包装器,该包装器里面对这个属性进行了一次封装并对这个封装进行存储管理。也可以理解为,属性包装器语法只是具有getter和setter的属性的语法糖。

简单来说,Property Wrapper 是一层包装。 可以提供一套对属性的默认操作、处理,相同类型的属性都可以加上这层包装,以提高代码的复用性。属性包装器和属性本身分离开来的,可以将某些常见的行为抽象出来放在属性包装器中,使代码更加简洁、可读、易于维护。

官方示例

理论上来说,定义属性包装器,可以创建一个结构、枚举或类来定义wrappedValue属性。在下面是一个最简单的属性包装器示例,其大概逻辑是:TwelveOrLess结构确保其包装的值始终包含小于或等于12的数字。如果你要求它存储一个更大的数字,它会存储12。

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

使用上面的TwelveOrLess,定义一个 SmallRectangle:

less 复制代码
struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

执行以下代码可得到的输出分别为:

scss 复制代码
var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"
 
rectangle.height = 10
print(rectangle.height)
// Prints "10"
 
rectangle.height = 24
print(rectangle.height)
// Prints "12"

上面代码其实很简单易懂,在 rectangle.height = 24 赋值的时候,经过了min(newValue, 12)函数处理,number 被赋值了12,而非24。print(rectangle.height) 打印值就是 12。

对于上面SmallRectangle 的定义,其实可以理解为下面的代码:

kotlin 复制代码
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 做了一次特殊处理,这种处理封装在 wrappedValue 的 set 方法中。

尝试自定义一个包装器

下面尝试写一个处理小数位的包装器,这个包装器,会自动将数据保留两位小数,数据类型用string包裹:

typescript 复制代码
@propertyWrapper
struct numberDecimals {
    private var number = ""
    var wrappedValue: String {
        get { return number }
        set {
            let newStr = NSString(string: newValue)
            if newStr.length <= 0 {
                number = newValue
            } else {
                number = String(format: "%.2f", newStr.floatValue)
            }
        }
    }
}

以下面这段代码为例:

dart 复制代码
@numberDecimals var num: String
        num = "1"
        print("num: (num)")
        num = ""
        print("num: (num)")

输出结果为:

综上,有以下几点目前是可以明确的:

  • 定义属性包装器,需要使用 @propertyWrapper 关键词声明。

  • 属性包装器必须要有 wrappedValue 属性(名字是固定的)。

  • 对属性的操作,可以自定义private var value

包装器中的属性也可以赋值初始数据

从结构来看,包装器本身也是一个Struct 或者 Class,那么,我们是不是也可以给他初始化方法呢?答案是可以的,扩展一下上面的保留小数位的包装器,增加属性decimalNum ,用来自定义小数位。如下代码:

typescript 复制代码
@propertyWrapper
struct numberDecimals {
    private var number = ""
    private var decimalNum = 2
    
    var wrappedValue: String {
        get { return number }
        set {
            let newStr = NSString(string: newValue)
            if newStr.length <= 0 {
                number = newValue
            } else {
                let decimalNumStr = String(format: "%%.%df", decimalNum)
                number = String(format: decimalNumStr, newStr.floatValue)
            }
        }
    }
    // 默认构造器
    init() {
        number = ""
        decimalNum = 2
    }
    init(wrappedValue: String) {
        number = String(format: "%.2f", NSString(string: wrappedValue).floatValue)
        decimalNum = 2
    }
    // 指定小数位数的构造器
    init(decimalNum: Int) {
        self.decimalNum = decimalNum
    }
    init(wrappedValue: String, decimalNum: Int) {
        self.decimalNum = decimalNum
        let decimalNumStr = String(format: "%%.%df", decimalNum)
        number = String(format: decimalNumStr, NSString(string: wrappedValue).floatValue)
    }
}

上面这段代码,定义了一个属性包装器,这个包装器比最开始的numberDecimals多了一个属性decimalNum,这个属性用来控制number被保留几位小数。numberDecimals是一个struct,那么它也可以有自己的初始化方法。可以声明初始化方法,那么就允许外部指定保留小数位的数量(decimalNum)。

以下面代码为例:

java 复制代码
 @numberDecimals(decimalNum: 3) var num3: String
    num3 = "1"
    print("num3: (num3)")

输出结果为:

不使用初始化与使用初始化对比:

less 复制代码
// base
@numberDecimals var num: String
    num = "1"
    print("num: (num)")
        
// initDecimal
@numberDecimals(decimalNum: 3) var num3: String
    num3 = "1"
    print("num3: (num3)")

输出结果:

说到上面代码,这里有一点建议:

  • 属性包装器最好是Struct类型。Struct是值类型,会自动管理引用,不用担心循环引用。

属性包装器的投影值 projectedValue

除了封装的值之外,属性包装器还可以通过定义投影值来公开其他功能------例如,管理对数据库访问的属性包装器可以在其投影值上公开flushDatabaseConnection()方法。投影值的名称与包装值的名称相同,只是以美元符号( <math xmlns="http://www.w3.org/1998/Math/MathML"> )开头。因为您的代码不能定义以 )开头。因为您的代码不能定义以 </math>)开头。因为您的代码不能定义以开头的属性,所以投影值永远不会干扰您定义的属性。

在上面例子中,增加一些内容:

ini 复制代码
@propertyWrapper
struct numberDecimals {
    private var number = ""
    private var decimalNum = 2
    private(set) var projectedValue: Bool = false
    
    var wrappedValue: String {
        get { return number }
        set {
            let newStr = NSString(string: newValue)
            if newStr.length <= 0 {
                number = newValue
                projectedValue = false
            } else {
                let decimalNumStr = String(format: "%%.%df", decimalNum)
                number = String(format: decimalNumStr, newStr.floatValue)
                projectedValue = true
            }
        }
    }
    ...
}

定义一个简单的结构体,并调用numberDecimals:

dart 复制代码
struct numberBox {
    @numberDecimals(decimalNum: 3) var num3: String
}

var box = numberBox()
    box.num3 = "2"
    print("boxNum: (box.num3), boxNumProjectValue: (box.$num3)")
    box.num3 = ""
    print("boxNum: (box.num3), boxNumProjectValue: (box.$num3)")

输出结果为:

属性包装器可以返回任何类型的值作为其投影值。在本例中,属性包装器只公开一条信息------是否格式化了number------因此它将该布尔值公开为其投影值。需要公开更多信息的包装器可以返回其他类型的实例,也可以返回self以将包装器的实例作为其投影值公开。

当您从属于类型的代码(如属性getter或实例方法)中访问投影值时,可以省略self。在属性名称之前,就像访问其他属性一样。在这个例子中,就是 $num3

swift 复制代码
struct numberBox {
    @numberDecimals(decimalNum: 3) var num3: String
    
    func checkHadDecimals() {
        print("boxNum had Decimals: ($num3)")
    }
}

var box = numberBox()
box.num3 = "2"
print("boxNum: (box.num3), boxNumProjectValue: (box.$num3)")
        
box.checkHadDecimals()

输出结果为:

如何让projectedValue返回self以获取更多信息呢?可以像下面这样修改代码:

kotlin 复制代码
// Define the projected value as an instance of the wrapper type itself.
var projectedValue: numberDecimals {
    return self
}

仍然以上面 box.num3 = "2",box.num3 = ""处代码为例,可以得到以下输出:

那么上面 具体是怎么访问又是指向谁呢?用下面代码验证一下:

scss 复制代码
    mutating func changeDecimals() {
        /// 这里的_num3访问的是包装器的实例,因此可以调用reSetDecimalNum(num: )方法
        /// 但是从 numberBox 外部调用 box._num3 就会产生错误 '_num3' is inaccessible due to 'private' protection level
         _num3.reSetDecimalNum(num: 4)
        // $符号是访问包装器属性projectedValue的一个语法糖
        $num3.welcome()

//        // ❌: Cannot use mutating member on immutable value: '$num3' is immutable
//        $num3.reSetDecimalNum(num: 5)
//        // ❌: Referencing instance method 'welcome()' requires wrapper 'numberDecimals'
//        num3.welcome()
//        // ❌: Cannot use mutating member on immutable value: 'self' is immutable
//        num3.reSetDecimalNum(num: 5)

         print(num3)  // 访问的wrappedValue, 输出:2.000 // _num3.reSetDecimalNum(num: 4) 之后就是 2.0000
         print(_num3) // 访问的是 wrapper type itself, numberDecimals(number: "2.000", decimalNum: 3)
         print($num3) // 访问的是projectedValue, numberDecimals(number: "2.000", decimalNum: 3)
    }

可以得到下面这些结论:

  • $符号是访问包装器属性的一个语法糖

  • num3: 访问的wrappedValue

  • _num3: 访问的是 wrapper type itself

  • $num3: 访问的是projectedValue

使用限制

  • 协议的属性不支持使用属性包装器

❌: property 'some' declared inside a protocol cannot have a wrapper.在协议中声明的属性'some'不能有包装器

swift 复制代码
protocol SomeProtocol {
    @numberDecimals var some: Int { get set }
}
  • extension中不可以使用

❌:Non-static property 'numExt' declared inside an extension cannot have a wrapper 在扩展内部声明的非静态属性"numExt"不能有包装

kotlin 复制代码
extension numberBox {
    @numberDecimals(decimalNum: 3) var numExt: String {
        return ""
    }
}
  • enum中不可以使用

❌:Property wrapper attribute 'numberDecimals' can only be applied to a property 属性包装属性"numberDecimals"只能应用于属性

typescript 复制代码
enum SomeEnum: String {
     @numberDecimals case one
     case two
 }
  • class里的 wrapper property 不能重写

❌:Cannot override with a stored property 'some' 无法用存储的属性"some"重写

kotlin 复制代码
class SomeClass {
    @numberDecimals var some: String
}
class OtherClass: SomeClass {
    override var some: String = "1"
}
  • wrapper 不能定义 gettersetter 方法

❌:Property wrapper cannot be applied to a computed property 属性包装器无法应用于计算属性

swift 复制代码
struct SomeStruct {
    @numberDecimals var some: String { return "test" }
}
  • wrapper 属性不能被 lazy@NSCopying@NSManagedweak、 或者 unowned 修饰

更多案例

基础介绍:

Swift Property Wrappers: Property wrappers

Property wrappers in Swift:Property wrappers in Swift | Swift by Sundell

进阶:

CodableWrappers:GitHub - GottaGetSwifty/CodableWrappers: A Collection of PropertyWrappers to make custom Serializati

ValidatedPropertyKit:GitHub - SvenTiigi/ValidatedPropertyKit: Easily validate your Properties with Property Wrappers 👮

相关推荐
良技漫谈2 天前
Rust移动开发:Rust在iOS端集成使用介绍
后端·程序人生·ios·rust·objective-c·swift
KeithTsui2 天前
ZFC in LEAN 之 前集的等价关系(Equivalence on Pre-set)详解
开发语言·其他·算法·binder·swift
袁代码3 天前
Swift 开发教程系列 - 第4章:函数与闭包
ios·swift·ios开发
安泽13144 天前
高德地图美食
开发语言·swift·美食
袁代码4 天前
Swift 开发教程系列 - 第2章:Swift 基础语法
swift·ios开发·基础教程
袁代码4 天前
Swift 开发教程系列 - 第1章:Swift 简介与开发环境配置
swift·ios开发·基础教程
孚亭4 天前
一些swift问题
swift
莫问alicia4 天前
echarts 实现3D饼状图 加 label标签显示
前端·3d·echarts·swift
uiop_uiop_uiop7 天前
iOS Swift5算法恢复——HMAC
ios·iphone·swift
東三城8 天前
【ios】---SwiftUI开发从入门到放弃
ios·swiftui·swift·1024程序员节