init
の時にメンバ変数(property)の didSet
が機能しないのは知っていたのですが、
deinit
時に機能しない?のを知らなくて、振り返ってみるとちょっと怯えたのでそのメモ。
initでpropertyのdidSet/willSetは呼ばれない
以下のように init
内ではdidSet/willSetは呼ばれません。
class Foo {
var a: String? {
didSet {
print("didSet!!")
}
}
init() {
a = "bar"
}
}
let foo = Foo()
// === output ===
ちなみに、 init
内で defer
をかますとdidSetが呼ばれるようにはなります。
class Foo {
var a: String? {
didSet {
print("didSet!!")
}
}
init() {
defer {
a = "bar"
}
}
}
let foo = Foo()
// === output ===
didSet!!
じゃあdeinitでは?
じゃあ deinit
では?ということで試してみました。
class Hoge {
var a: String? {
didSet {
b = a
}
}
var b: String?
deinit {
print("\(#function)")
a = nil // これ
output()
}
func output() {
print("a:", a as Any)
print("b:", b as Any)
}
}
do {
let h = Hoge()
h.a = "Hoge"
h.output()
print("===")
h.a = nil
h.output()
print("===")
h.a = "Fuga"
h.output()
}
//============
// didSet!!
// a: Optional("Hoge")
// b: Optional("Hoge")
// ===
// didSet!!
// a: nil
// b: nil
// ===
// didSet!!
// a: Optional("Fuga")
// b: Optional("Fuga")
// deinit
// a: nil
// b: Optional("Fuga") // !!!!!!!
なんと、、今までdeinitで変数に値を入れる(例えばnilを入れる)とかした場合に、 didSet
が呼ばれて、
本来してほしかった処理が動いていると思ったら動いていなかったようです。
この例だと、deinitで a = nil
をした時に、didSetが呼ばれて b = a (= nil)
を期待したのですが、そうはならなかったです。
deferをかますと…?
init
と同じように、 deinit
でも defer
をかますと同様に didSet
が機能するようになります。。
class Hoge {
var a: String? {
didSet {
print("didSet!!")
b = a
}
}
var b: String?
deinit {
defer {
print("\(#function)")
a = nil
output()
}
}
func output() {
print("a:", a as Any)
print("b:", b as Any)
}
}
do {
let h = Hoge()
h.a = "Hoge"
h.output()
print("===")
h.a = nil
h.output()
print("===")
h.a = "Fuga"
h.output()
}
// ==================
// didSet!!
// a: Optional("Hoge")
// b: Optional("Hoge")
// ===
// didSet!!
// a: nil
// b: nil
// ===
// didSet!!
// a: Optional("Fuga")
// b: Optional("Fuga")
// deinit
// didSet!! // deinit内での代入で呼び出されている
// a: nil
// b: nil // ちゃんとnilになっている!
謎だ。
動いたとしても、deinit
の処理が通った後にdeferで色々やるのは避けたい所。
こんなケースでは意図した動作しないかも…
極端な例ですが、
class Baz: NSObject {
dynamic var value: String = ""
override class func automaticallyNotifiesObservers(forKey key: String) -> Bool {
if key == "value" {
return true
}
return super.automaticallyNotifiesObservers(forKey: key)
}
}
class Foo: NSObject {
var baz: Baz? {
willSet {
baz?.removeObserver(self, forKeyPath: "value")
}
didSet {
baz?.addObserver(self, forKeyPath: "value", options: [.new, .old], context: nil)
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "value" {
print(keyPath as Any, object as Any)
}
}
deinit {
baz = nil // willSet/didSetが動くのを期待
}
}
do {
let f = Foo()
let baz = Baz()
f.baz = baz
baz.value = "!!!"
}
// doブロックを抜けた時にクラッシュする
のように、baz
という変数が代入された時に、add/removeObserverするような設計をしていた場合に、
deinit
でnil入れてるから、willSet
, didSet
呼ばれるから大丈夫でしょうと油断していると実はremoveできていなくて後にクラッシュする…
なんて可能性もあります。
なので、deinit
時はwillSet,didSetに頼らないように処理を記述するのが良いと思われます。
上記の例では、以下のようにするとdoブロックを抜けた時にクラッシュしなくなります。
deinit {
baz?.removeObserver(self, forKeyPath: "value")
}
なるべく
defer
をかますのはややトリッキーになるので、init/deinit時は didSet/willSet
に頼らず自分で必要な処理を記述するようにしましょう。
今回のサンプルはGist1, Gist2にあげてあります。