1.什么是属性包装器(Property Wrappers)
属性包装器是在管理属性存储方式的代码和定义属性的代码之间添加了一层分离。
管理属性的set、get方式由属性包装器接管;
定义属性的代码还在原来的类中;
例如,如果您有提供线程安全检查或将其基础数据存储在数据库中的属性,则必须在每个属性上编写该代码。当您使用属性包装时,您在定义包装时编写一次管理代码,然后通过将其应用于多个属性来重用该管理代码。
简单讲,Property Wrapper 是对属性的一层封装,隐藏与属性相关的逻辑细节,提高代码的复用性。
因为它将属性包装器和属性本身分离开来,并将某些常见的行为抽象出来,使代码更加简洁、可读、易于维护。
A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property.
属性包装器,用来修饰属性,它可以抽取关于属性重复的逻辑来达到简化代码的目的。
For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property. When you use a property wrapper, you write the management code once when you define the wrapper, and then reuse that management code by applying it to multiple properties.
比如:如果你有属性提供了线程安全检查或将数据存到数据库功能,那么你将需要为每个属性编写类似代码。有了属性包装器,我们就可以避免类似重复代码。
个人理解:属性包装器是对 set、get方法的封装,不同的属性有相似的set、get,使用属性包装器可以简化相似的代码。
如何使用属性包装器
To define a property wrapper, you make a structure, enumeration, or classthat defines a wrappedValue property.
我们通过 @propertyWrapper 来标识structure, enumeration, or class来实现属性包装
有两个要求
- 必须使用属性@propertyWrapper进行定义。
- 它必须具有wrappedValue属性。
@propertyWrapper
struct NumberWrapper {
// 这是包装的属性
var wrappedValue: Int
}
struct Number {
// 告诉编译器使用Wrapper包装器包装该属性
@NumberWrapper var number: Int
}
如何理解 其作用是将属性的 定义代码
与属性的存储方式代码
进行分离 ?
需求要求: 存储的number不可以大于10.
@propertyWrapper
struct NumberWrapper {
private var value: Int = 0
var wrappedValue: Int {
get {
return value
}
set {
value = min(newValue, 10)
}
}
}
struct Number {
// 告诉编译器使用Wrapper包装器包装该属性
@NumberWrapper var number: Int
}
将属性包装器改成这样的实现,被修饰的number属性,就具有了永不大于10的逻辑。
那么,当使用属性包装时,实际发生了什么呢?在通过对属性包装时编译器会自动转为下面的代码:
struct Number {
private var _number = NumberWrapper()
var number: Int {
get { return _number.wrappedValue }
set { _number.wrappedValue = newValue }
}
}
也就是Number.number= 24 这句代码的调用路径:
- 调用 Number 中number 的 set 函数
- 调用 属性包装器的 _number.wrappedValue 的 set函数
- 调用 number = min(newValue, 10) 来保证新设置的值小于等于 10
再来看一个例子:
这是一个姓氏拾取器,通过传入的人名,拾取第一个字作为姓氏。
@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 登场。
@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
我们通过属性包装器成功的从名字中获取到了姓氏,在此基础上在实现获取姓氏的中文拼音首字母用来排序,应该怎么处理呢?
@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 提供辅助能力。
@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. 给属性包装器设置初始值
还从最简单的例子开始
@propertyWrapper
struct NumberWrapper {
private var value: Int = 0
var wrappedValue: Int {
get {
return value
}
set {
value = min(newValue, 10)
}
}
}
struct Number {
// 告诉编译器使用Wrapper包装器包装该属性
@NumberWrapper var number: Int
}
如果我们直接给number设置默认值,发现会报错。
根据报错的提示,加上对属性包装器的理解,其实是NumberWrapper这个结构体缺少一个初始化方法,init(wrappedValue: Int), 添加初始化方法后可就可以使用了
@propertyWrapper
struct NumberWrapper {
private var value: Int = 0
var wrappedValue: Int {
get {
return value
}
set {
value = min(newValue, 10)
}
}
// 参数一定要叫wrappedValue, 不能修改
init(wrappedValue: Int) {
self.value = wrappedValue
}
}
struct Number {
// 告诉编译器使用Wrapper包装器包装该属性
@NumberWrapper var number: Int = 5
}
这2种写法完全等效, 都会进到 init(wrappedValue: Int) 这个方法
- @NumberWrapper var number: Int = 5
- @NumberWrapper(wrappedValue: 15) var number4: Int
属性包装器也可以有自己的参数
注意:wrappedValue必须在第一个参数,并且名字不能修改
@propertyWrapper
struct NumberWrapper {
private var value: Int = 0
private var maxValue: Int = 10
var wrappedValue: Int {
get {
return value
}
set {
value = min(newValue, self.maxValue)
}
}
// 参数一定要叫wrappedValue, 不能修改
init(wrappedValue: Int = 0) {
self.value = wrappedValue
}
// 属性包装器的自定义初始化方法
init(wrappedValue: Int, max: Int) {
self.maxValue = max
self.wrappedValue = wrappedValue
}
}
struct Number {
// 告诉编译器使用Wrapper包装器包装该属性
@NumberWrapper var number: Int = 5
// number2和number的效果等价, 注意wrappedValue必须在第一个参数
@NumberWrapper(wrappedValue: 50, max: 100) var number2: Int
@NumberWrapper(max: 100) var number3: Int = 50
}
这2种写法也是完全等价。
- @NumberWrapper(wrappedValue: 50, max: 100) var number2: Int
- @NumberWrapper(max: 100) var number3: Int = 90
4. 访问属性包装器
@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'不能有包装器
protocol SomeProtocol {
@TenOrLess var some: Int { get set }
}
5.2 extension中不可以使用
❌:Non-static property 'some' declared inside an extension cannot have a wrapper. 在扩展内声明的非静态属性'some'不能有包装器.
extension Rectangle {
@TenOrLess var some: Int { return 5 }
}
5.3 enum中不可以使用
❌:Property wrapper attribute 'TenOrLess' can only be applied to a property. 属性包装器属性'TenOrLess'只能应用于属性
enum SomeEnum: Int {
@TenOrLess case one
case two
}
5.4 class里的 wrapper property
不能重写
❌:Cannot override with a stored property 'some'. 不能用存储属性“some”重写
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. 属性包装器不能应用于计算属性
struct SomeStruct {
@TenOrLess var some: Int {
return 0
}
}
5.6 wrapper
属性不能被 lazy
、 @NSCopying
、 @NSManaged
、 weak
、 或者 unowned
修饰
6. 其他的一些示例
6.1 验证传入的字符串值是否为空。
@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提供一个便利的调用方法。
@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
6.3属性加锁使用
@propertyWrapper
class LockAtomic<T> {
private var value: T
private let lock = NSLock()
public init(wrappedValue value: T) {
self.value = value
}
public var wrappedValue: T {
get { getValue() }
set { setValue(newValue: newValue) }
}
// 加锁处理获取数据
func getValue() -> T {
lock.lock()
defer { lock.unlock() }
return value
}
// 设置数据加锁
func setValue(newValue: T) {
lock.lock()
defer { lock.unlock() }
value = newValue
}
}
使用LockAtomic
@LockAtomic
var json: [String: String]?
json = ["a": "1"]
print(json) // Optional(["a": "1"])
Alamofire 中使用属性包装器, 使用@Protected包裹的属性, set/get 方法都能保证线程安全。
/// A thread-safe wrapper around a value.
@propertyWrapper
@dynamicMemberLookup
final class Protected<T> {
private let lock = UnfairLock()
private var value: T
init(_ value: T) {
self.value = value
}
/// The contained value. Unsafe for anything more than direct read or write.
var wrappedValue: T {
get { lock.around { value } }
set { lock.around { value = newValue } }
}
var projectedValue: Protected<T> { self }
init(wrappedValue: T) {
value = wrappedValue
}
// ...
}
6.4使用Codable协议时设置默认值,避免服务器确实数据导致解析失败
@propertyWrapper
struct DefaultWrapper<T: Codable>: Codable {
private var value: T
var wrappedValue: T {
get {
return self.value
}
set {
self.value = newValue
}
}
init(wrappedValue: T) {
self.value = wrappedValue
}
}
class MusicModel: NSObject, Codable {
@DefaultWrapper var musicId: Int = 0
@DefaultWrapper var author: String = ""
}
链接:https://www.jianshu.com/p/9b4523dac4f2
官方文档
参考1:【Swift】属性包装器注解@propertyWrapper