Closureを使っていたら、ちょっと予想外の動作になってしまっていました。 超・基本的なことですが、動作確認したのでメモ。
closureの循環参照と weak self
weak self
をなぜ使うかというと、循環参照によるメモリーリークを避けるためですよね。
closureを保持するクラスを作って見てみましょう。
下の Personクラスは、生成・消滅した時がわかりやすいように init
と deinit
にログをいれておきます。
そシンプルに Hello, I am [name]!
と表示してくれる normalHello
というメソッドを作り、 keepAndDo
メソッドで、closureを保持するようにしておきます。
このPersonクラスをつかって、Closureを保持して実行してみましょう。
strongHello
では weak self
を使わず、 weakHello
では weak self
を使っています。
実行してみると、🦁の deinit
だけよばれません。
weak self
をよんでいないから self
が循環参照をおこして解放されず、メモリーリークの原因になっていますね。
escapingとnonescape
で、ここで escaping
と nonescape
についても復習しましょう。
対象となるclosureがスコープから抜けても存在するときには @escaping
が必要になります。
上の keepAndDo
メソッドでは @escaping
をつけていますが、これを省略するとコンパイルエラーになります。
上の場合は、keepAndDo
のスコープから抜けても、(selfで保持しているので)closureが存在するから必要なんですね。
で、似たような処理でnoescapeのものを付け加えてみましょう。
さきほどの Personクラスに、closureを実行するだけで保持しない justDo
メソッドをつけてみました。
その justDo
を よぶ noescapeHello
には weak self
はついていません。
(なお、Swift3からデフォルトは @noescape
になったので、最近は @noescape
を書くことはないと思います。)
実行してみると、weak self
をつけなくても deinit
がよばれています。循環参照は発生していません。
escapingで非同期実行
で、さらにここで非同期実行をかけてみましょう。 それぞれのclosureのなかに、1秒後に自分の名前のログを出す処理をいれておきます。
さらに、 noescapeAndWeakHello
として、非同期実行のclosureで weak self
つきのメソッドも追加。
で、これを実行すると……。
weakで実行した場合、当然解放済みなので非同期実行で名前が表示されないんですが、noescapeだと名前が表示されるのに、noescapeかつweakだと名前が表示されない……。
ログを見ればわかることですが、noescapeの場合、非同期処理の完了と同時にPersonのインスタンスが削除されますが、selfにweakがついていると、非同期処理が実行される前にインスタンスが削除されてしまうんですね。 weak self
つけてるんだから当然ですけど。
ステップで一つ一つ確認すれば当然の動作なんだけど、惰性で weak self
つけちゃって予想外の動作になっちゃっていました。
なんとなく weak self
、じゃなくてちゃんと理由を考えて使わないとな。
(とおもいつつ、closureの処理が深くなってくると、だんだんわからなくなるんですよね……。)
基本的には closureは nonescapeでとりまわすのが一番事故らない気はしますが、いろいろな事情でclosureをkeepしたいこともあり。難しいところです。