Swift 一直以来有个很是方便的特性:属性观察者(Property Observer),即属性上的willSet
和 didSet
函数。在 Swift 5.3 中,对 didSet
有一处小的性能优化,在了解这个以前,咱们来仔细复习一下 didSet
,有一些细节你不必定知道或者记得。面试
最多见的使用场景是给正在定义的类型中的存储属性添加观察者,以下例所示,willSet
在属性值被更新以前被调用,didSet
在属性值更新后被调用。swift
class Container {
var items = [Int](repeating: 1, count: 100) {
willSet {
print("willSet is called")
print("current item size: \(items.count)")
print("new item size: \(newValue.count)")
}
didSet {
print("didSet is called")
print("current item size: \(items.count)")
print("old item size: \(oldValue.count)")
}
}
}
let container = Container()
container.items = [] // willSet 和 didSet 被调用
复制代码
在willSet
和 didSet
中能够访问属性自己,而且若是不指定名称的话,willSet
能够经过 newValue
访问即将被设置的值,didSet
能够经过oldValue
访问这次设置以前的属性值。安全
这里须要注意的是,哪怕设置的值与原来的值相同,willSet
和 didSet
都是会被调用的。性能优化
有一种状况下属性观察者不会被调用:当前类型的init
函数中,假若有如下init
函数,在构造的时候对 items
进行赋值并不会触发 willSet
和 didSet
。app
class Container {
init() {
items = []
}
}
复制代码
为何要制定这一条规则呢?缘由在于构造函数原本就是特殊的,假如在构造的时刻触发属性观察者,而属性观察者中又访问了还没被初始化的其它的属性的话,就致使了访问了未彻底初始化的对象,Swift 主打安全的初始化就会被破坏。ide
在继承状况下的规则有一些不一样,可是也好理解。函数
第一点:在构造函数中,对继承而来的属性设置值会触发父类中的属性观察者的调用。工具
class MyContainer: Container {
var tag: String
override init() {
tag = "Leon"
super.init()
items = [1,2,3] // 触发父类中的 willSet 和 didSet
}
}
复制代码
这时候你可能会有个问号:这里的触发难道不会形成访问一个未彻底初始化的对象吗?假如父类的 didSet
中调用的一个方法被子类 override,而子类的这个方法中访问了还没被初始化的子类中声明的属性?性能
访问未被初始化的子类属性的状况不存在,由于在调用 super.init()
前,子类必须完成自身属性的初始化。实际上,上述代码段中的三行初始化的语句是没有办法调换顺序的。Swift 经过强制初始化顺序来确保在复杂状况下构造函数仍是安全的,避免了一些成熟的面相对象语言中存在的问题。优化
第二点:能够给继承的属性添加属性观察者,哪怕继承的是计算属性:
class MyContainer: Container {
override var items: [Int] {
didSet {
print("didSet is called in the subclass")
}
}
}
复制代码
不管父类中的属性是否有属性观察者,做为子类我只添加,而且按照先父类再子类的顺序来执行。另一点,对于计算属性咱们也能够添加属性观察者了,由于做为子类来讲,针对继承的属性添加属性观察者,无需区分它究竟是存储属性仍是计算属性了,它就是属性(getter 和 setter)。
话说回来,为何不能为本身声明的计算属性添加 willSet
和didSet
,那是由于你是个成熟的计算属性了,set
原本就是你自身定义的了,你在开头写上 willSet
的逻辑,在结束写上 didSet
这就起到属性观察者的一样做用了。
若是属性是一个值类型,调用它的 mutating
方法或者直接修改它的值的话会从内到外逐层调用属性观察者。
下面这个例子中,会先调用 items
的 didSet
,再调用 container
的didSet
,注意这个例子中的 Container
已经改为值类型了。
struct Container {
var items = [Int](repeating: 1, count: 100) {
didSet {
print("items didSet is called")
}
}
}
class ViewController: UIViewController {
var container = Container() {
didSet {
print("container didSet is called")
}
}
override func viewDidLoad() {
super.viewDidLoad()
container.items.append(1)
}
}
复制代码
假如 Container
是个引用类型,那么只有 items
的 didSet
会被调用,引用类型做为属性,只有在该引用被替换的时候才会触发属性观察者。
当参数是由 inout 修饰的时候,咱们须要知道在函数结束的时候,不管有没有修改,属性都会被写回,这是由 Swift 内存模型所规定的。
struct Container {
var items = [Int](repeating: 1, count: 100) {
willSet {
print("item willSet is called")
}
didSet {
print("item didSet is called")
}
}
}
func modify(items: inout [Int]) {
print("actually do nothing")
}
var container = Container()
modify(items:&container.items)
复制代码
这个例子中,首先会打印的是 actually do nothing
,而后是 willSet
和 didSet
在 Swift 5.3 中,提供了一个简单版本的 didSet
:若是 oldValue
没有被用到(如上例),Swift 5.3 会直接跳过 oldValue
的建立,这意味着节省了 内存 和 CPU 的开销。
这个改动有很小的可能会影响到代码兼容性,好比说代码的正确性依赖于属性 的 getter
被调用。要恢复这个行为能够显示地声明变量名字:
didSet(oldValue) {}
复制代码
或者这样引用一下:
didSet { _ = oldValue }
复制代码
咱们可使用计算属性来证实一下这件事情:
class Container {
var items :[Int] {
get {
print("getter is called")
return []
}
set {}
}
}
class MyContainer: Container {
override var items: [Int] {
didSet {
print("didSet is called")
}
}
}
let container = MyContainer()
container.items = []
复制代码
在 Swift 5.3 中,getter
不会被调用,性能获得了提高。
属性观察者是个很是实用的工具,除了能够平常进行 debug 或者打日志,还能够用来实现一些简单逻辑,好比说 ViewController
中有一个 person
属性,当它被更新的时候,我能够在 didSet
调用更新界面的逻辑。
另外,咱们能够在本身声明的属性的didSet
中安全地从新给这个属性设置一个新的值,这不会触发didSet
的无限循环调用。
你必定以为这很棒啊,有什么须要注意的吗?因而开始浪,在继承属性的didSet
中也对本属性赋值了新的值,那恭喜你无限循环崩溃了。
你想了下,这怎么多是我,那还有一种状况可能适合你,在 A 属性的 didSet
中更新 B 属性的值,并在 B 属性的 didSet
中更新 A 属性的值。你是风儿我是沙,缠缠绵绵栈爆炸。
也许这种错误还不是你这种级别的高手会犯的,但一旦代码分支复杂了,联动效果多了,不是谁能一眼看出来了,说不定哪天就变成了蝴蝶效应。无节制的使用 didSet 的联动是明显的代码坏味道,尽管能够经过判等跳过来打破这个循环,临时解决这个问题,可是不让代码往这个方向腐化是每一个代码维护者须要关心的事情。
扫码下方二维码关注“面试官小健”