变量(variable)是任何一门语言的构成基础, 对 Swift 也不例外. 大家可能会想, 这东西还有什么"细节"可言, 正常使用不就行了.
其实, 在 Swift 中, 变量使用起来还是有一些细节问题需要注意以及提防的, 作者就根据自己的实际经验总结了如下一些东西, 仅供大家参考.
由于这里探讨的是使用细节问题, 因此基本的变量声明、操作以及相关概念等此文便不再赘述.
本文将分成如下 3 部分进行说明.
1. stored variable 和 computed variable
2. Observer(观察器)
3. 懒加载(重点说明!!!)
1. stored variable 和 computed variable
Swift 中的变量分为 stored variable 和 computed variable.
stored variable 顾名思义, 就是我们一般意义上理解的可以进行赋值和取值的变量.
var number: Int = 10
上面的 number 就是一个 stored variable, 对其进行正常的读(取值)写(赋值)操作即可, 因为 stored variable 是可以存储值的.
computed variable, 字面意思为"计算型的变量", 虽然也叫"变量", 但要注意, 这个家伙是根本没法存储值的.
来看下面的例子, 这里定义了一个名为 number 的 computed variable:
var number: Int {
get {
return 10
}
set {
print("number 被设置为了 \(newValue)")
}
}
下面对 number 进行我们"想当然"的读写操作:
number = 100 // 输出结果为 " number 被设置为了 100 "
print(number) // 输出结果为 " 10 "
What? 明明给 number 赋了新值 100, 并且输出结果也明确指出 number 被设置为了 100, 为什么再次打印 number 的值时, 却还是 10 ???
切记, computed variable 根本不会存储我们主观上认为的"值"!!! 它实质上是一些待执行的代码!!!
当 number 被赋值时, set 中的代码被执行. 同理, 当执行读取 number 的操作时, get 中的代码被执行.
再次重申, computed variable 只是一些待执行的代码!!!
在对 computer variable 赋值时, 我们能够取到赋给其的新值, 那就是 set 中的 newValue, 至于这个值要怎么用, 那就看实际的需要了. 但要注意, newValue 不会存储至 computed variable中! 毕竟, computed variable 只是一些待执行的代码! 重要的事情说三遍!!!
可能有人会想, 毕竟 computed variable 叫做"变量", 如果不能像 stored variable 那样进行很直观的读写操作, 总觉得很奇怪, 我下面给出了一种用 computed variable 实现 stored variable 效果的思路.
这里首先定义一个 PositiveNumber 类, 用来表示正数:
class PositiveNumber {
private var _value: Double = 0.0
var value: Double {
get {
return _value
}
set {
_value = newValue
}
}
}
我为 PositiveNumber 类定义了两个属性, 分别为私有的store property _value 和可供外部访问的接口: 一个 computed property value.
是不是很面熟? 没错, 这跟 OC 中定义一个 @property 是完全相同的!
let number = PositiveNumber()
number.value = 10 // 利用 computed property 的 set 方法为 _value 赋值
print(number.value) // 利用 computed property 的 get 方法读取 _value 的值
看到这里, 你可能会想, 如此大费周折, 只做了一件 stored property 很容易就能做到的事儿, 这不是闲的吗?
我们仔细思考一下上例, number 是一个 PositiveNumber 的实例, 在上述实现方式下, 我们可以为 number.value 赋任意值, 如: number.value = -10
这在逻辑上明显是不合适的, 因为 number 是一个"正数", 所以我们要对赋给 number.value 的值进行一定的处理. 这种逻辑交给 computed property 就再合适不过了.
修改上述代码如下:
class PositiveNumber {
private var _value: Double = 0.0
var value: Double {
get {
return _value
}
set {
if (newValue <= 0) {
print("赋值不合理!")
} else {
_value = newValue
}
}
}
}
这样, 若再执行 number.value = -10
则会执行判断的逻辑, 从而使对 _value 的赋值操作更加严谨.
当然, computed property 的作用不仅如此, 下面说到懒加载时还会提到.
2. Observer(观察器)
Observer(即 willSet 和 didSet 方法) 是用在 stored variable 上的, 毕竟只有能存储值的东西才有被观察的价值!
基本的使用方法这里就不赘述了, 只对使用时的一些细节做一下说明.
- 当 stored variable 被初始化时, 观察器方法不会被调用
- 如果在 didSet 方法中改变了 stored variable 本身的值, 观察器方法也不会被调用. 这个逻辑很容易理通, 因为若在观察器方法中修改 stored variable 的值还会调用观察器方法的话, 那么便会导致观察器方法的无限循环调用!
- 在 willSet 方法中去操作 stored variable, 其实是对旧值进行操作, 新值为 newValue
- 在 didSet 方法中去操作 stored variable, 其实是对新值进行操作, 旧值为 oldValue
参考下面的代码
class PositiveNumber {
var value: Double {
willSet {
print("willSet 方法被调用")
print("在 willSet 中, value = \(value), newValue = \(newValue)")
}
didSet {
print("didSet 方法被调用")
print("在 didSet 中, value = \(value), oldValue = \(oldValue)")
}
}
init(value: Double) {
self.value = value
}
}
此处测试一下
let number = PositiveNumber(value: 10.0)
此时, 控制台未输出任何信息! 可见这个初始化的操作并没有调用观察器方法.
再来执行 number.value = 20
控制台输出信息
willSet 方法被调用
在 willSet 中, value = 10.0, newValue = 20.0
didSet 方法被调用
在 didSet 中, value = 20.0, oldValue = 10.0
由上例可以看到观察器调用时的一些细节, 大家在使用时注意一下就好.
3. 懒加载
对于开发者来说,懒加载最被人熟知的优点就在于只在需要某个 variable 时, 才去进行加载.其实, 懒加载还能处理一些普通的初始化方法处理不了的情况.
以初始化一个类的实例为例, 在普通的初始化方法中, 若该实例的初始化过程还未结束, 那么开发者是无法在该初始化方法中引用该实例的属性和方法的. 而懒加载则可以解决这个问题, 因为当需要懒加载某个属性时, 该实例变量已经初始化完毕, 因此开发者可以在懒加载的流程中去任意使用该实例的属性以及方法!
下面说明一下懒加载的使用细节.
- 全局变量默认都是懒加载的! 其实仔细想想, 苹果的这种设计还是非常合理的. 对于一个全局变量来说, 何时加载它最合适呢? 显然程序一启动就加载的方式不太合理, 于是乎, 还是在需要它时再加载吧!
- static 的属性默认都是懒加载的! static 的属性某种程度上同全局变量很类似, 例如一个 struct 中含有 static 的属性, 那么这个属性是属于这个 struct 类型的, 而不是属于某个具体的 struct 变量! 于是问题又来了, 何时加载这个属性最合适? 依然还是需要它时再加载吧!
- 实例属性默认都不是懒加载的! 注意! 都不是懒加载的! 若要令其变为懒加载, 那就在声明时加 lazy, 并且这个实例属性必须是 var, 而不能是 let. 同时, 懒加载的实例属性也无法实现观察器方法, 即没有 willSet 和 didSet 方法.
虽然 Swift 已经提供给了我们一种超级给力的实现懒加载的方式, 即只要一句 lazy 就搞定了, 但上述最后一条细节还是留给了我们很多遗憾!
考虑这样一种需求: 某个实例属性一定要采用懒加载的方式, 而且要对其实现观察器的功能, 即可观测其值的变化并进行一定的逻辑处理.
再考虑另一种需求: 某个实例属性一定要采用懒加载的方式, 并且加载完成后是只读的, 不能对其进行修改.
乍一看, 这些需求明显是在给 Swift 原生的懒加载方式找茬嘛! 上述需求, 原生的 lazy var 一条都实现不了!!! 首先, 懒加载的实例属性本身就无法实现观察器方法, 同时 lazy var 这种形式的声明又导致该实例变量可以被修改, 此时会想, 如果有个 lazy let 就好了...
但是, 上述需求并不过分, 开发中也会碰到, 怎么办? 只能手写一个满足上述需求的懒加载了. 下面只提供了一种思路, 仅供大家参考
class PositiveNumber {
private var token: dispatch_once_t = 0
private var _value: Double = 0.0
var value: Double {
get {
dispatch_once(&token) {
// 一些非常耗时的初始化 _value 的流程
}
return _value
}
set {
_value = value
}
}
}
其实仔细想想, 懒加载在某种程度上同 computed variable 是相同的, 即只有需要时才调用! 那么就可以利用 computed variable 的这些特点来人造一个懒加载方式.
上段代码采用了本文中第 1 点提到的方式, 实现了通过一个 computed variable 来操作一个 stored variable 的功能, 然后利用 GCD 的一次性代码实现了懒加载的"只加载一次"操作. 至此, 我们手写了一个与使用 lazy 来实现懒加载操作相同的方法.
但是, 要注意! 上段代码的灵活性更强一些, 包括:
- 若想实现观察器方法, 只要在 set 方法中构建相应的逻辑即可. 例如, 在
_value = value
前添加 willSet 中希望具有的逻辑, 在_value = value
后添加 didSet 中希望具有的逻辑即可 - 若想实现 lazy let 形式的懒加载, 只需要把 set 方法去掉即可, 那么属性初始化后就变成只读的了, 外界无法对其进行任何修改
- 采用了 GCD 的一次性代码, 可以保证线程安全. 根据 Apple 官方的《The Swift Programming Language》, 普通的采用 lazy var 形式的懒加载, 无法保证线程安全. 一旦某个线程对某一实例属性的懒加载过程未结束, 而另一个线程同时又操作了该实例属性, 那么会导致又一次加载该属性, 此时该属性便被初始化了多次, 已不再具有"懒"的特点了