Swift 属性包装器简介
了解 Swift 属性包装器是什么,并探索它们如何提供一种有效的方法,使计算属性能够在不同类之间重复使用。
现在,已经介绍了 Swift 中的类和结构体,本章将引入一个相关主题:属性包装器。自 Swift 5.1 起引入的属性包装器提供了一种减少在类和结构体实现中编写 getter、setter 和计算属性时重复代码的方法。
理解属性包装器
当通过类或结构体实例访问或赋值属性时,有时需要对值进行某种转换或验证后再存储或读取。正如之前“Swift 面向对象编程”课程中所述,这种行为可以通过创建计算属性来实现。然而,常常会出现多个类或结构体共用相同计算属性的情况。在 Swift 5.1 之前,共享计算属性逻辑的唯一方式是复制代码并嵌入到每个类或结构体的实现中。这不仅效率低下,而且一旦计算逻辑发生变化,必须手动在所有使用该逻辑的实体中进行更新。
为解决这一问题,Swift 5.1 引入了名为 属性包装器 的特性。属性包装器本质上允许将计算属性的功能从具体的类和结构体中分离出来,并在整个应用代码库中复用。
一个简单的属性包装器示例
理解属性包装器的最佳方式是通过一个简单示例。假设有一个结构体包含一个 String 类型的属性,用于存储城市名称。该结构体可能如下所示:
1struct Address { 2 var city: String 3}
如果要求该类以大写形式存储城市名称,无论用户输入的是什么格式,可以在结构体中添加如下计算属性:
1struct Address { 2 3 private var cityname: String = "" 4 5 var city: String { 6 get { cityname } 7 set { cityname = newValue.uppercased() } 8 } 9}
当给属性赋值时,计算属性中的 setter 会将值转换为大写后存入私有变量 cityname(通过调用 String 类的 uppercased() 方法实现)。可以使用以下代码测试该结构体:
1var address = Address() 2 3address.city = "London" 4print(address.city)
显然,计算属性完成了将城市名称转为大写的功能。但如果其他结构体或类也需要同样的行为,就必须重复编写这段代码。虽然在这个例子中代码量很小,但更复杂的计算逻辑可能会导致大量重复代码。
与其使用计算属性,也可以将此逻辑实现为属性包装器。例如,下面的声明定义了一个名为 FixCase 的属性包装器,用于将 String 值转为大写:
1@propertyWrapper 2struct FixCase { 3 var value: String = "" 4 5 var wrappedValue: String { 6 get { value } 7 set { value = newValue.uppercased() } 8 } 9 10 init(wrappedValue initialValue: String) { 11 self.wrappedValue = initialValue 12 } 13}
属性包装器使用 @propertyWrapper 指令声明,通常以结构体形式实现(结构体是首选)。所有属性包装器都必须包含一个名为 wrappedValue 的属性,其中包含更改或验证值的 getter 和 setter 代码。可选地,还可以添加一个初始化器,接收被赋值的初始值。在此例中,初始值被直接赋给 wrappedValue,然后转换为大写并存储在私有变量中。
定义好这个属性包装器后,就可以在需要相同行为的其他属性上重复使用。只需在类或结构体中需要的地方,用 @FixCase 指令前缀属性声明即可。例如:
1struct Contact { 2 @FixCase var name: String 3 @FixCase var city: String 4 @FixCase var country: String 5} 6 7var contact = Contact(name: "John Smith", city: "London", country: "United Kingdom") 8print("\(contact.name), \(contact.city), \(contact.country)")
支持多个变量和类型
在上述示例中,属性包装器仅接受一个值,即要赋给被包装属性的值。更复杂的属性包装器还可以接受其他参数,在执行计算时使用。这些额外参数放在属性包装器名称后的括号中。例如,一个用于限制数值在指定范围内的属性包装器可能如下所示:
1struct Demo { 2 @MinMaxVal(min: 10, max: 150) var value: Int = 100 3}
实现上述 MinMaxVal 属性包装器的代码如下:
1@propertyWrapper 2struct MinMaxVal { 3 var value: Int 4 let max: Int 5 let min: Int 6 7 init(wrappedValue: Int, min: Int, max: Int) { 8 value = wrappedValue 9 self.min = min 10 self.max = max 11 } 12 13 var wrappedValue: Int { 14 get { return value } 15 set { 16 if newValue > max { 17 value = max 18 } else if newValue < min { 19 value = min 20 } else { 21 value = newValue 22 } 23 } 24 } 25}
注意,init() 方法除了接收被包装的值外,还接收 min 和 max 值。wrappedValue 的 setter 会检查新值,若超出范围则将其调整为 min 或 max。
可以使用以下代码测试该属性包装器:
1struct Demo { 2 @MinMaxVal(min: 100, max: 200) var value: Int = 100 3} 4 5var demo = Demo() 6demo.value = 150 7print(demo.value) 8 9demo.value = 250 10print(demo.value)
执行后,第一个 print 语句输出 150,因为该值在允许范围内;第二个 print 语句显示值被限制为最大允许值(即 200)。
目前实现的属性包装器仅适用于整数(Int)类型。如果能支持任何可比较的类型,将会更加实用。幸运的是,属性包装器可以设计为支持符合特定协议的任意类型。由于该包装器的目的是进行比较,因此可以修改它以支持符合 Comparable 协议的任何数据类型。该协议属于 Swift 标准库的一部分。符合 Comparable 协议的类型可以用于相等性、大于和小于比较。许多类型如 String、Int、Date、DateInterval 和 Character 都符合此协议。
为了使包装器能够与任何符合 Comparable 协议的类型一起使用,需将声明修改如下:
1@propertyWrapper 2struct MinMaxVal<V: Comparable> { 3 var value: V 4 let max: V 5 let min: V 6 7 init(wrappedValue: V, min: V, max: V) { 8 value = wrappedValue 9 self.min = min 10 self.max = max 11 } 12 13 var wrappedValue: V { 14 get { return value } 15 set { 16 if newValue > max { 17 value = max 18 } else if newValue < min { 19 value = min 20 } else { 21 value = newValue 22 } 23 } 24 } 25}
修改后的包装器仍可像以前一样处理 Int 类型。但它现在还能用于任何符合 Comparable 协议的其他类型。例如,以下示例中字符串值会被检查是否在最小和最大字符串值之间的字母顺序范围内:
1struct Demo { 2 @MinMaxVal(min: "Apple", max: "Orange") var value: String = "" 3} 4 5var demo = Demo() 6demo.value = "Banana" 7print(demo.value) 8 9demo.value = "Pear" 10print(demo.value)
第一个 print 语句输出 "Banana",因为它在字母顺序范围内,且成功存储。第二个 print 语句输出 "Orange",因为 "Pear" 超出了字母顺序范围,因此被设置为允许的最大值。
同样,该包装器也适用于 Date 实例。以下示例中,值被限制在当前日期与一个月后之间:
1import Foundation 2 3struct DateDemo { 4 @MinMaxVal(min: Date(), max: Calendar.current.date(byAdding: .month, 5 value: 1, to: Date())! ) var value: Date = Date() 6} 7 8var dateDemo = DateDemo() 9print(dateDemo.value) 10 11// 属性设置为十天后 12dateDemo.value = Calendar.current.date(byAdding: .day, value: 10, to: Date())! 13print(dateDemo.value) 14 15// 属性设置为两个月后 16dateDemo.value = Calendar.current.date(byAdding: .month, value: 2, to: Date())! 17print(dateDemo.value)
首先,注意上述示例需要导入 Foundation 框架才能编译:
1import Foundation
Foundation 框架包含一组在 Swift 开发中非常有用的类。此处导入模块是为了访问 Swift 的 Date 类。
当执行上述代码时,第一行输出为运行代码的服务器当前日期和时间(可能与本地时间不同)。第二行输出为十天后的日期时间(在允许范围内)。最后,由于两个月后超出了允许范围,第三行输出显示最大允许日期为一个月后。
总结
自 Swift 5.1 起引入的属性包装器,允许将原本应放在属性 getter 和 setter 中的行为提取出来,并在整个应用项目代码库中复用,从而避免在类和结构体声明中重复代码。属性包装器以结构体形式声明,使用 @propertyWrapper 指令。
属性包装器是 Swift 中一项强大的功能,使你能够为 Swift 语言添加自定义行为。
课程回顾
-
属性包装器允许计算属性在多个类声明中复用。
-
属性包装器以类或结构体形式声明,通过
@propertyWrapper指令告知编译器。 -
所有属性包装器必须包含一个名为
wrappedValue的属性,其中包含更改或验证值的 getter 和 setter 代码。
测验
掌握 Swift 属性包装器的知识对于未来使用 SwiftUI 开发应用程序至关重要。请回答以下测验题,确保你理解了核心概念。
练习
利用这次机会,完成以下练习,编写自己的属性包装器。
练习 1
实现一个名为 DoubleInt 的属性包装器,用于将下方 Number 结构体中的 Int 值翻倍(解决方案位于练习下方)。
1struct Number { 2 3 var number: Int 4 5 init(_ number: Int) { 6 self.number = number 7 } 8} 9 10let demo = Number(200) 11print(demo.number)
解答
1@propertyWrapper 2struct DoubleInt { 3 var value: Int = 0 4 5 var wrappedValue: Int { 6 get { value } 7 set { value = newValue * 2 } 8 } 9 10 init(wrappedValue initialValue: Int) { 11 self.wrappedValue = initialValue 12 } 13} 14 15struct Number { 16 17 @DoubleInt var number: Int 18 19 init(_ number: Int) { 20 self.number = number 21 } 22} 23 24let demo = Number(200) 25print(demo.number)
Quiz
属性包装器的主要目的是什么?