Swift 5.3 新特性精講(3):屬性觀察者以及didSet的性能優化

Swift 一直以來有個很是方便的特性:屬性觀察者(Property Observer),即屬性上的willSetdidSet 函數。在 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 被調用
複製代碼

willSetdidSet 中能夠訪問屬性自己,而且若是不指定名稱的話,willSet 能夠經過 newValue訪問即將被設置的值,didSet能夠經過oldValue訪問這次設置以前的屬性值。安全

這裏須要注意的是,哪怕設置的值與原來的值相同,willSetdidSet都是會被調用的。性能優化

有一種狀況下屬性觀察者不會被調用:當前類型的init函數中,假若有如下init函數,在構造的時候對 items 進行賦值並不會觸發 willSetdidSetapp

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)。

話說回來,爲何不能爲本身聲明的計算屬性添加 willSetdidSet,那是由於你是個成熟的計算屬性了,set原本就是你自身定義的了,你在開頭寫上 willSet 的邏輯,在結束寫上 didSet 這就起到屬性觀察者的一樣做用了。

屬性類型:值與引用

若是屬性是一個值類型,調用它的 mutating方法或者直接修改它的值的話會從內到外逐層調用屬性觀察者。

下面這個例子中,會先調用 itemsdidSet,再調用 containerdidSet,注意這個例子中的 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 是個引用類型,那麼只有 itemsdidSet會被調用,引用類型做爲屬性,只有在該引用被替換的時候纔會觸發屬性觀察者。

inout 例行公事

當參數是由 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,而後是 willSetdidSet

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

掃碼下方二維碼關注「面試官小健」

相關文章
相關標籤/搜索