Properties 特点

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

Property wrappers

Property Wrappers:特点包装器,Swift 官方文档对其界说是:特点包装器是在办理特点存储的代码和界说特点的代码之间添加一层别离。例如,假如您的特点需求供给线程安全检查或将其基础数据存储在数据库中,您需求在每个特点上编写该代码。当您运用特点包装器时,您只需在界说包装器时编写一次办理代码,然后经过将其应用于多个特点来重用该办理代码。

我了解的是:界说特点时,指定一个特点包装器,该包装器里面临这个特点进行了一次封装并对这个封装进行存储办理。也能够了解为,特点包装器语法仅仅具有getter和setter的特点的语法糖。

简略来说,Property Wrapper 是一层包装。 能够供给一套对特点的默许操作、处理,相同类型的特点都能够加上这层包装,以提高代码的复用性。特点包装器和特点本身别离开来的,能够将某些常见的行为笼统出来放在特点包装器中,使代码愈加简洁、可读、易于维护。

官方示例

理论上来说,界说特点包装器,能够创建一个结构、枚举或类来界说wrappedValue特点。在下面是一个最简略的特点包装器示例,其大概逻辑是:TwelveOrLess结构确保其包装的值始终包括小于或等于12的数字。假如你要求它存储一个更大的数字,它会存储12。

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

运用上面的TwelveOrLess,界说一个 SmallRectangle:

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

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

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 的界说,其实能够了解为下面的代码:

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包裹:

@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)
            }
        }
    }
}

以下面这段代码为例:

@numberDecimals var num: String
        num = "1"
        print("num: (num)")
        num = ""
        print("num: (num)")

输出成果为:

探索 Property wrappers  in Swift

综上,有以下几点现在是能够明确的:

  • 界说特点包装器,需求运用 @propertyWrapper 关键词声明。

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

  • 对特点的操作,能够自界说private var value

包装器中的特点也能够赋值初始数据

从结构来看,包装器本身也是一个Struct 或者 Class,那么,咱们是不是也能够给他初始化办法呢?答案是能够的,扩展一下上面的保存小数位的包装器,增加特点decimalNum ,用来自界说小数位。如下代码:

@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)。

以下面代码为例:

 @numberDecimals(decimalNum: 3) var num3: String
    num3 = "1"
    print("num3: (num3)")

输出成果为:

探索 Property wrappers  in Swift

不运用初始化与运用初始化比照:

// base
@numberDecimals var num: String
    num = "1"
    print("num: (num)")
// initDecimal
@numberDecimals(decimalNum: 3) var num3: String
    num3 = "1"
    print("num3: (num3)")

输出成果:

探索 Property wrappers  in Swift

提到上面代码,这里有一点主张:

  • 特点包装器最好是Struct类型。Struct是值类型,会主动办理引用,不必担心循环引用。

特点包装器的投影值 projectedValue

除了封装的值之外,特点包装器还能够经过界说投影值来揭露其他功用——例如,办理对数据库拜访的特点包装器能够在其投影值上揭露flushDatabaseConnection()办法。投影值的称号与包装值的称号相同,仅仅以美元符号()最初。因为您的代码不能界说以)最初。因为您的代码不能界说以最初的特点,所以投影值永远不会搅扰您界说的特点。

在上面比如中,增加一些内容:

@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:

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)")

输出成果为:

探索 Property wrappers  in Swift

特点包装器能够回来任何类型的值作为其投影值。在本例中,特点包装器只揭露一条信息——是否格式化了number——因而它将该布尔值揭露为其投影值。需求揭露更多信息的包装器能够回来其他类型的实例,也能够回来self以将包装器的实例作为其投影值揭露。

当您从属于类型的代码(如特点getter或实例办法)中拜访投影值时,能够省掉self。在特点称号之前,就像拜访其他特点一样。在这个比如中,便是 $num3

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()

输出成果为:

探索 Property wrappers  in Swift

怎样让projectedValue回来self以获取更多信息呢?能够像下面这样修正代码:

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

仍然以上面 box.num3 = “2”,box.num3 = “”处代码为例,能够得到以下输出:

探索 Property wrappers  in Swift

那么上面 详细是怎样拜访又是指向谁呢?用下面代码验证一下:

    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’不能有包装器

protocol SomeProtocol {
    @numberDecimals var some: Int { get set }
}
  • extension中不能够运用

❌:Non-static property ‘numExt’ declared inside an extension cannot have a wrapper 在扩展内部声明的非静态特点“numExt”不能有包装

extension numberBox {
    @numberDecimals(decimalNum: 3) var numExt: String {
        return ""
    }
}
  • enum中不能够运用

❌:Property wrapper attribute ‘numberDecimals’ can only be applied to a property 特点包装特点“numberDecimals”只能应用于特点

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

❌:Cannot override with a stored property ‘some’ 无法用存储的特点“some”重写

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 特点包装器无法应用于核算特点

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