[Swift] どうして weak var delegate とするのか?
伝えたいこと
delegate に適応した self を弱参照させるパターンは以下の2通りがある。
- 案1:
weak var
で宣言した変数を用意して、delegate に適応したself
を代入する- メリット:シンプルである
- デメリット:delegate 変数の外部からの変更や、delegete の代入忘れの恐れがある
- 案2:
lazy var
で delegate に適応したself
を init の引数で渡す。変数はprivate weak var
またはprivate unowned let
で外部からの変更を阻止する- メリット:
weak var
を使う場合にあったようなデメリットがなくなる - デメリット:
lazy var
を使う影響で、その変数が変更される恐れや、lazy var
の変数が使われるタイミングで init が走るので、そのタイミングによっては予期せぬバグが発生する(=lazy var
のリスク)- 案2-1:
private weak var
で変数を宣言した場合:- メリット:delegate が破棄された状態でアクセスした場合に、
delegate?
かdelegate!
で 実行スキップか、クラッシュかを選べる - デメリット:
var
であること
- メリット:delegate が破棄された状態でアクセスした場合に、
- 案2-2:
private unowned let
で変数を宣言した場合:- メリット:
let
であること - デメリット:delegate が破棄された状態でアクセスした場合に、必ずクラッシュする(むしろ、delegate が破棄されないことを想定しているのであれば、クラッシュすることによってバグの早期発見に繋がるのでメリットかもしれません。)
- メリット:
- 案2-1:
- メリット:
weak var delegate
とするのか?
どうして weak
をつけていないサンプルコードを用いて挙動を確認してみます。
weak をつけない場合の挙動
以下のようなサンプルコードを用意します。
protocol HogeDelegate: AnyObject {
func hogehoge()
}
class MogeClass {
var delegate: HogeDelegate?
deinit {
print("deinit: \(type(of: self))")
}
func mogemoge() {
delegate?.hogehoge()
}
}
class FugaClass {
let mogeClass: MogeClass
init() {
mogeClass = MogeClass()
mogeClass.delegate = self
}
deinit {
print("deinit: \(type(of: self))")
}
func fugafuga() {
mogeClass.mogemoge()
}
}
extension FugaClass: HogeDelegate {
func hogehoge() {
print("called hogehoge() on FugaClass")
}
}
fugaClass の fugafuga() の実行
var fugaClass: FugaClass? = FugaClass()
fugaClass?.fugafuga()
// (出力)
// called hogehoge() on FugaClass
期待通りですね。
fugaClass init して deinit する
var fugaClass: FugaClass? = FugaClass()
fugaClass = nil
// (出力)
// なし
あれ?
fugaClass
が解放されませんね。
さらに mogeClass
も解放する術も失っていますね。
weak をつけた場合の挙動
class MogeClass {
weak var delegate: HogeDelegate? // ← weak をつける
deinit {
print("deinit: \(type(of: self))")
}
func mogemoge() {
delegate?.hogehoge()
}
}
fugaClass init して deinit する
var fugaClass: FugaClass? = FugaClass()
fugaClass = nil
// (出力)
// deinit: FugaClass
// deinit: MogeClass
解放されますね!!
fugaClass の fugafuga() の実行して、それから fugaClass に nil を代入する
もちろん、fugaClass の fugafuga() の実行して、それから fugaClass に nil を代入しても解放されます。
var fugaClass: FugaClass? = FugaClass()
fugaClass?.fugafuga()
fugaClass = nil
// (出力)
// called hogehoge() on FugaClass
// deinit: FugaClass
// deinit: MogeClass
このように解放されないケースがあるので、weak var
と宣言する必要があります。
weak var
の微妙な点
- 変数を
var
で宣言するので、外から変更されてしまう可能性がある - delegate の変数に値を代入し忘れる可能性がある
weak var
の微妙な点の改善方法を検討する
lazy var
で self を渡す
改良案:init の引数に delegate を設定し、サンプルコードは以下になります。
protocol HogeDelegate: AnyObject {
func hogehoge()
}
class MogeClass {
let delegate: HogeDelegate // let に修正
init(delegate: HogeDelegate) {
self.delegate = delegate // init でもらうようにする
}
deinit {
print("deinit: \(type(of: self))")
}
func mogemoge() {
delegate.hogehoge()
}
}
class FugaClass {
lazy var mogeClass = MogeClass(delegate: self) // lazy var で self を渡すようにする
deinit {
print("deinit: \(type(of: self))")
}
func fugafuga() {
mogeClass.mogemoge()
}
}
extension FugaClass: HogeDelegate {
func hogehoge() {
print("called hogehoge() on FugaClass")
}
}
fugaClass の fugafuga() の実行
var fugaClass: FugaClass? = FugaClass()
fugaClass?.fugafuga()
// (出力)
// called hogehoge() on FugaClass
うん。問題ないですね。
fugaClass init して deinit する
var fugaClass: FugaClass? = FugaClass()
fugaClass = nil
// (出力)
// deinit: FugaClass
MogeClass が解放されていないように見えるが、そもそも生成されていないので、問題ないですね。
fugaClass の fugafuga() の実行して、それから fugaClass に nil を代入してみる
var fugaClass: FugaClass? = FugaClass()
fugaClass?.fugafuga()
fugaClass = nil
// (出力)
// called hogehoge() on FugaClass
あれ、、、解放されていないみたい。。。
let
→ weak var
にしてみる
結局、class MogeClass {
weak var delegate: HogeDelegate? // 結局 weak var にする
init(delegate: HogeDelegate) {
self.delegate = delegate
}
deinit {
print("deinit: \(type(of: self))")
}
func mogemoge() {
delegate?.hogehoge()
}
}
再Try
var fugaClass: FugaClass? = FugaClass()
fugaClass?.fugafuga()
fugaClass = nil
// (出力)
// called hogehoge() on FugaClass
// deinit: FugaClass
// deinit: MogeClass
うまくいきましたね。。。
でも結句 weak var
をつかってしまっております。。。
改良案の微妙な点
うまくいきましたが、lazy var
で self を渡すようにしても、やっぱり weak var
をつけなればいけないのですね。。。
ただ、private
をつければ、外からの変更は防げるので、それが気になる方はそうした方がいいかもしれませんね。
しかし、そもそも lazy var
としている方が、変更のリスクが発生しますが、、、どっちがいいんでしょうね。
また、private
をつけるのであれば、init での delegate の設定を忘れないように、delegate
を使う際は強制アンラップしてもよいかもしれないですね。
class MogeClass {
private weak var delegate: HogeDelegate? // private をつける
init(delegate: HogeDelegate) {
self.delegate = delegate
}
deinit {
print("deinit: \(type(of: self))")
}
func mogemoge() {
delegate!.hogehoge() // 強制アンラップにする
}
}
でも、init で設定しているはずなのに delegate?
や delegate!
とするのはなんかいけてませんよね。。。
weak let
があれば、解決できそうですが、weak let
を使おうとすると「error: 'weak' must be a mutable variable, because it may change at runtime
」と怒られます。
どうやら weak let
はないみたいですね。。。
unowned let
を使う!
改良案の改良案:weak let
はできないですが、unowned let
であれば宣言できるみたいでした。
実際に使ってみます。
class MogeClass {
private unowned let delegate: HogeDelegate // unowned なら let を宣言できる
init(delegate: HogeDelegate) {
self.delegate = delegate
}
deinit {
print("deinit: \(type(of: self))")
}
func mogemoge() {
delegate.hogehoge() // ? や ! も消せる
}
}
再Try
var fugaClass: FugaClass? = FugaClass()
fugaClass?.fugafuga()
fugaClass = nil
// (出力)
// called hogehoge() on FugaClass
// deinit: FugaClass
// deinit: MogeClass
うまくいきましたね。
lazy var
で delegate となる self
を init で渡すような場合は、unowned let
を使っても良さそうですね。
ただ、lazy var
を使うことのリスクもあるので、それには十分注意が必要です。
unowned let
のリスクについて
lazy var
で delegate となる self
を init で渡すようにする際には、unowned let
でもよさそうなのですが、init で渡した delegate が途中で破棄されるような場合は、この使い方だとクラッシュするリスクがあります。
以下のような、HogeDelegate
に適応した PiyoClass
を用意して、これを使って、MogeClass
を生成してみます。
class PiyoClass: HogeDelegate {
deinit {
print("deinit: \(type(of: self))")
}
func hogehoge() {
print("called hogehoge() on PiyoClass")
}
}
再Try
var piyoClass: PiyoClass? = PiyoClass()
let mogeClass = MogeClass(delegate: piyoClass!)
mogeClass.mogemoge()
piyoClass = nil
// (出力)
// called hogehoge() on PiyoClass
// deinit: PiyoClass
一見、問題なさそうですが、piyoClass = nil
としたあとに再び、mogeClass.mogemoge()
を実行してみます。
var piyoClass: PiyoClass? = PiyoClass()
let mogeClass = MogeClass(delegate: piyoClass!)
mogeClass.mogemoge()
piyoClass = nil
mogeClass.mogemoge() // error: Execution was interrupted, reason: signal SIGABRT.
クラッシュしました。
MogeClass
で渡したはずの HogeDelegate
がないので、当然ですよね。
unowned let
はやっぱり良くないのか?
では、もちろん、unowned let
ではなく weak var
に変更して、MogeClass
での delegate 使用時に delegate?
とアクセスすればクラッシュはさけられます。
ただ、それはアプリを使用するユーザーにとって良いことなのかは、ケースバイケースだと思います。
そもそも、思わぬタイミングで破棄されるような書き方になっていることも問題ですし、潔くクラッシュして、できるだけ早く不具合を発覚して、修正したほうがいいという考え方もあります。
今回の場合も、unowned let
が問題というよりは、piyoClass
を nil
許容の変数にしたり、MogeClass
の生成時に、piyoClass
を強制アンラップしているのが問題な気がします。
裏を返すと、ここまでしないと、unowned let
でクラッシュは起こせないです。
もちろん weak var
でも delegate!
とすれば、このケースでもクラッシュを起こせるので、どれを使うかはやっぱりケースバイケースだと思います。
銀の弾丸はないということですね。。。
結論
delegate に適応した self を弱参照させるパターンは以下の2通りがある。
- 案1:
weak var
で宣言した変数を用意して、delegate に適応したself
を代入する- メリット:シンプルである
- デメリット:delegate 変数の外部からの変更や、delegete の代入忘れの恐れがある
- 案2:
lazy var
で delegate に適応したself
を init の引数で渡す。変数はprivate weak var
またはprivate unowned let
で外部からの変更を阻止する- メリット:
weak var
を使う場合にあったようなデメリットがなくなる - デメリット:
lazy var
を使う影響で、その変数が変更される恐れや、lazy var
の変数が使われるタイミングで init が走るので、そのタイミングによっては予期せぬバグが発生する(=lazy var
のリスク)- 案2-1:
private weak var
で変数を宣言した場合:- メリット:delegate が破棄された状態でアクセスした場合に、
delegate?
かdelegate!
で 実行スキップか、クラッシュかを選べる - デメリット:
var
であること
- メリット:delegate が破棄された状態でアクセスした場合に、
- 案2-2:
private unowned let
で変数を宣言した場合:- メリット:
let
であること - デメリット:delegate が破棄された状態でアクセスした場合に、必ずクラッシュする(むしろ、delegate が破棄されないことを想定しているのであれば、クラッシュすることによってバグの早期発見に繋がるのでメリットかもしれません。)
- メリット:
- 案2-1:
- メリット:
Discussion