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 的聯動是明顯的代碼壞味道,儘管能夠經過判等跳過來打破這個循環,臨時解決這個問題,可是不讓代碼往這個方向腐化是每一個代碼維護者須要關心的事情。
掃碼下方二維碼關注「面試官小健」