Swift 属性
属性是类型和实例携带的数据,Swift 把它们分成存储属性和计算属性两大类。
存储属性真正占用内存保存一个值,计算属性则每次访问时现算,不占存储空间。
本文讲存储与计算属性、只读计算属性、属性观察器、延迟属性 lazy,以及容易被写错的类型属性 static 与 class 的区别。
存储属性与计算属性
存储属性用 var 或 let 声明,直接保存一个值。
计算属性必须用 var 声明,通过 get 和 set 两个代码块来读取和写入,本身不存储任何数据。
实例
var originX: Double = 0.0 // 存储属性:真实保存一个值
var width: Double = 0.0 // 存储属性
var centerX: Double { // 计算属性:每次访问时计算
get { originX + width / 2 }
set { originX = newValue - width / 2 }
}
}
var rect = RunoobRect(originX: 10, width: 20)
print(rect.centerX) // 读取时执行 get
rect.centerX = 50 // 写入时执行 set,newValue 就是 50
print(rect.originX)
20.0 40.0
set 代码块里可以直接用 newValue 这个默认名字,也可以写成 set(newCenter) { ... } 自己指定参数名。
注意:计算属性不能写成 let。let 表示值一旦确定就不再变化,而计算属性本身没有值,只有每次访问时才产生的计算结果。
只读计算属性
只提供 get、不提供 set 的计算属性就是只读计算属性,此时 get 关键字可以省略。
实例
var side: Double
// 只有 get,可以省略 get 关键字
var area: Double { side * side }
// 显式写出 get 也完全等价
var perimeter: Double {
get { side * 4 }
}
}
let square = RunoobSquare(side: 3)
print(square.area, square.perimeter)
9.0 12.0
只读计算属性可以像普通属性一样用点语法读取,但尝试给它赋值会编译报错。
用 let 声明的结构体实例也能读取只读计算属性,因为它只读不写。
属性观察器 willSet 与 didSet
属性观察器用来监听存储属性的变化,willSet 在赋值之前调用,didSet 在赋值之后调用。
最容易被忽略的一点是:初始化阶段给属性赋值不会触发观察器。
实例
var total: Int {
willSet { print("willSet: \(total) -> \(newValue)") }
didSet { print("didSet: \(oldValue) -> \(total)") }
}
init(total: Int) {
self.total = total // 初始化时不触发观察器
}
}
let counter = RunoobCounter(total: 5)
print("初始化完成,total = \(counter.total)")
counter.total = 10 // 这才是赋值,观察器会被触发
初始化完成,total = 5 willSet: 5 -> 10 didSet: 5 -> 10
观察器的参数名也可以自己指定,不写就用默认的 newValue 和 oldValue。
实例
var value: Int = 0 {
willSet(newValue) { print("即将写入 \(newValue)") }
didSet(oldValue) { print("旧值 \(oldValue)") }
}
}
let score = RunoobScore()
score.value = 90
即将写入 90 旧值 0
属性观察器只能加在存储属性上;在子类里重写继承来的属性时,也可以给它补上观察器,这部分看《Swift 继承》。
注意:在 init 里给属性赋初值不会触发 willSet 和 didSet,只有初始化完成之后的赋值才会。如果初始化时也需要执行某些逻辑,应该直接写在 init 里。
延迟存储属性 lazy
用 lazy 修饰的存储属性在第一次被访问时才计算初始值,之后一直复用这个结果。
它适合初始化开销大、或者依赖其他属性的场景。
实例
lazy var content: String = {
print("开始加载数据……")
return "www.runoob.com 的数据"
}()
}
let loader = RunoobLoader()
print("实例已创建")
print(loader.content) // 第一次访问,闭包执行
print(loader.content) // 第二次访问,直接返回缓存结果
实例已创建 开始加载数据…… www.runoob.com 的数据 www.runoob.com 的数据
lazy 属性必须用 var 声明,因为它第一次被访问时值才会被写入,而 let 属性在初始化后就不能再赋值。
注意:lazy 属性不是线程安全的。多个线程同时第一次访问同一个 lazy 属性,可能导致闭包执行多次,并发场景要自己加锁或改用其他方案。
类型属性:static 与 class 的区别
类型属性属于类型本身,不属于任何一个实例,用类型名访问。
存储型类型属性必须用 static;class 只能用来修饰可被重写的计算型类型属性,不能修饰存储属性。
实例
static var siteName = "RUNOOB" // 存储型类型属性:必须用 static
static let version = "1.0" // 常量类型属性也用 static
}
print(RunoobConfig.siteName, RunoobConfig.version)
class RunoobBase {
static var stored = "存储型,子类不可重写"
static var staticComputed: String { "静态计算型" }
// class 只能修饰可重写的计算型类型属性
class var overridable: String { "父类的计算型类型属性" }
}
class RunoobSub: RunoobBase {
override class var overridable: String { "子类重写后的计算型类型属性" }
}
print(RunoobBase.overridable)
print(RunoobSub.overridable)
print(RunoobSub.stored) // 存储型类型属性可以被子类继承访问
RUNOOB 1.0 父类的计算型类型属性 子类重写后的计算型类型属性 存储型,子类不可重写
| 关键字 | 能否修饰存储属性 | 能否被子类重写 | 可用位置 |
|---|---|---|---|
static | 能 | 不能 | 结构体、枚举、类都可以 |
class | 不能 | 能 | 只能用在类的计算属性、方法、下标上 |
常见的写法是把共享常量声明成 static let,把需要子类定制的行为声明成 class var 或 class func。
注意:不要写 class var name = "...",存储型类型属性用 class 会直接编译报错,必须改成 static。
全局变量与局部变量
在类型之外、文件作用域声明的变量叫全局变量,在函数内部声明的叫局部变量。
全局变量和类型属性默认都是延迟初始化的,第一次访问时才创建,这一点和 lazy 一致;局部变量则是在执行到声明语句时初始化。
实例
var runoobGlobal = "www.runoob.com"
func showLocal() {
var local = "局部变量"
local += ",只在函数内可见" // 局部变量可以修改
print(local)
}
showLocal()
print(runoobGlobal)
局部变量,只在函数内可见 www.runoob.com
全局变量同样可以加属性观察器,写法与普通存储属性一致。
实例
didSet { print("全局变量被修改为 \(runoobCount)") }
}
runoobCount = 5
全局变量被修改为 5
常见问题
下面是属性相关的几个高频疑问。
计算属性能不能加属性观察器
不能。观察器只能加在存储属性上,因为计算属性每次访问都会执行代码,再包一层观察器没有意义。
需要在计算属性里做额外处理,直接把逻辑写进 get 和 set 即可。
lazy 和计算属性有什么区别
lazy 是真正的存储属性,值算出来之后会保存下来,第二次访问不再重复计算。
计算属性每次访问都会重新执行 get,不会缓存结果,两者要按「结果是否需要复用」来选择。
static 和 class 到底怎么选
先看要写的是存储属性还是计算属性:存储属性只能用 static。
如果是计算属性,再看需不需要子类重写:需要重写用 class,不需要就用 static。
为什么属性观察器在 init 里没触发
初始化阶段的任务是给属性建立初始值,此时属性还没有「旧值」,观察器自然不适用。
官方明确规定了这一点,所以依赖 didSet 做校验或同步的逻辑,要额外考虑初始化路径。
