今回も引き続き クロージャー
による 強参照循環
について見ていきます。前回まででその様子を窺うためのサンプルコードを書いていったので、今日はそこから実際の循環参照の様子についてを細かく眺める回になりそうです。よろしくお願いしますね。
————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #311
00:00 Start 01:25 とりあえず復習 01:52 必要になったときに準備する 03:19 初期化フェイズに初期化を閉じ込めることは重要 04:42 オプショナルも乱用しないのが安全そう 06:00 遅延初期化では self を利用可能 08:49 クロージャーによる強参照循環を試す 11:14 強参照循環の様子 13:29 サンプルコードでなければ問題点の多いコード 17:02 self の参照数について 18:43 インスタンスが解放されない様子を観察する 20:45 必要なものだけ個別にキャプチャーする方法も 23:33 初期化式を差し替えてキャプチャーを回避してみる 25:15 所有権を使って開放できる? —————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #311
はい、それでは始めていきますね。今日は引き続き、クロージャーにおける順番参照について見ていきます。この問題が具体的に発生する理由を探るためのサンプルコードを前回までに作成しました。このサンプルコードには、クロージャーにおける順番参照が起こる例が埋め込まれており、その具体的な対策をこれから見ていきます。
前のスライドで紹介した部分を少し詳しく見ていく必要があります。コードを用意した部分まで見てみましょう。この例では、HTMLプロパティ
を作成し、それがカスタマイズ可能になっています。前回参加していない人には少し分かりづらいかもしれませんが、まずはスライドを読んでいきます。
このHTMLプロパティ
は遅延評価のプロパティとして定義されています。前回も述べましたが、遅延評価は必要になるまで避けたほうが良いという考え方があります。ただし、必要になった時だけ評価を行うという方法も合理的だと考えられます。レイジーの基本的な役割は、初めて参照されたときに値を設定することです。それだけの理由でレイジーを使うのも一つの価値観と言えるでしょう。
私は個人的に、初期化のタイミングが分かりづらくなりデバッグに支障をきたすことがあるので、なるべく避けたいと考えています。ただ、こういった考え方が頭でっかちなのかもしれませんし、安全性を重視しすぎているのかもしれません。どちらにしても、レイジーについて考える余裕は持っていたほうが良いと思います。コードの安全性を考えると、可能な限り初期化を先送りにしないほうが無難だと感じます。
レイジーは、コンパイルタイムで済ませられることをランタイムに持ち越さないほうが安全なコードが書ける、という考えから、初期化フェーズに閉じ込めておいた方がいいとされています。初期化フェーズでは外部からアクセスできないようにしておくことで、初期化を確実に完了させる仕組みになっています。持ち越されると、何が起こるか分かりにくくなるので、そういう意味でレイジーは少し注意が必要です。
話を戻しますが、まずHTML
が遅延評価のプロパティである事実は、初期値のクロージャー内でself
を参照できるという特徴があります。初期値としてクロージャーの中でself
を使う場合には、それを最大限利用すべきです。しかし、コードが複雑になる恐れもあるので、使い方には気を付ける必要があります。
まとめると、HTML
のような遅延評価のプロパティは、その利用方法に注意が必要です。そして、その特性を理解して慎重に使うように心がけるのが良いでしょう。 最初にHTMLプロパティがレイジーとして定義されているのは、必要になるまで値を計算しないためです。イニシャライザーについては重要ですが、今回はその詳細には触れず進めますね。前回に取り上げた別の内容とは違い、スライドにある内容に目を向けてみますと、パラグラフがHTMLのイベントとしてオプショナルであり、変数として定義していることがあります。これは再代入して開放できるようにしたいという意図がありますので、特別な対処は今回は行いません。
ここでは、<p>
タグで"Hello World"を出力する簡単なサンプルを紹介しています。コードはlet paragraph = "<p>Hello World</p>"
のように書かれ、HTMLで出力すると、この<p>
タグを通して"Hello World"がコンテンツとして表示されることを確認できます。ここでアスタリスクが使われていますが、これは特別な理由があってのことです。コードを書く際、オプショナルを使うと早めの対処が必要になりますが、それがコードの安定性に寄与しているということですね。
今回の実行では、出力として<p>
タグの中に"Hello World"が表示されるはずですが、単にコードを確認するということであれば、出力がなかった場合にはそれを起点として次の議論に進みます。しかし、出力結果が見えているならそれを踏まえて次に進めます。
注目すべきは、次のスライドにあるイニシャライザーについてです。ここから先は、HTML要素による循環参照の例について述べています。現在のasHtml
というプロパティにはデフォルトのクロージャーがあり、それがself
をキャプチャしています。このため、循環参照が発生するのです。パラグラフ変数はHTMLエレメントを保持しており、このHTMLエレメントがasHtml
を通じて自己参照型の構造を作っています。このクロージャーは引数を取らず、文字列を返すものですが、その中でself.text
やself.name
を用いているため、クロージャーが実行されるまではself
が保持されてしまいます。
この状態を避けるには、コード全体でアズHTMLのプロパティがHTMLエレメントのインスタンスを共有し、循環参照を自己生成してしまうパターンを無くすことが大事です。慣れていないと、この循環参照は見落としがちになりますが、注意が必要です。この解説の内容からも分かるように、循環参照が発生する可能性が高いコードには十分細心の注意を払うべきですし、今回のサンプルコードでは無理やり循環参照を形成してしまうような構造になっています。このようなコーディングは避けた方が望ましいです。 今回のコードでは、循環参照に陥った場合の対策について考えてみたいと思います。安易にレイジーバーを使ったことが一つの問題であり、またクロージャをプロパティとして持たせたことも問題です。さらに、デフォルトの初期化方法でself
をキャプチャしたクロージャを持たせたことも大きな問題となります。こうしたコードにはさまざまな問題があると感じます。
そもそも、レイジーの使用に関しては慎重に行うべきというのが基本で、今回のコードは見直した方が良いかもしれません。循環参照を理解し、適切に対処できれば良いのですが、農機や仕事で忙しくどうしても完成させなければならない場合は、コメントを入れて無理やり作った旨を書いておく方法もあります。
この程度の問題でコードが大きく壊れることは少ないですが、危険な考え方なので、通常はきちんと理解しておくべきです。何か質問があればぜひ教えてください。
さて、進めた例の復習として話を戻ります。HTMLElement
インスタンスの場合ですと、html
というプロパティがクロージャを保持しているため、self
がキャプチャされています。self.name
やself.text
を利用する部分では、self
をキャプチャしてしまいます。これが重要なポイントです。
クロージャが循環参照を起こしています。クロージャにおけるself
のキャプチャについては、別のセクションで紹介されているようですが、まだそれを見る段階ではないので、後ほど詳しく解説します。この部分は、他のセクションを飛ばしてARC(Automatic Reference Counting)に進めてきたところからの復習です。
クロージャ内でself
を複数回参照している場合でも、HTMLElement
インスタンスの参照は一つです。この話は補足といえます。何度もself.
を使っていても、self
に対する参照が増えていないという点がキャプチャの考え方ですね。暗黙的に動いているので見えていない部分もあるため、この参照の仕組みについて仮にキャプチャ機能がないとしたら、クロージャに入っているときにself
が生存しているオーナーの変数を一度ローカルに代入する、といったようなイメージです。このように考えることでキャプチャというものが理解しやすくなります。
実際に、解放されなかった例としては、paragraph
変数に初期化を二回行っても、デイニシャライザーが動かず、解放がされなかった事例が挙げられます。これを解放できるようにするためには、キャプチャリストを利用する方法が簡単で優れた解決策として紹介されていましたが、具体的な例はスライドで紹介されているので一度置いておきます。
キャプチャリストを使わずに、このソースコード内で解放できるようにする方法を考えてみたいと思います。責任を持って、コードの中で循環参照を避ける方法を提案したいところです。 まず、共参照循環を解消する方法についての話ですが、これはプログラミングでしばしば問題となることです。特に、オブジェクトが互いを参照し続けることでメモリが解放されず、最終的にはメモリリークを引き起こす可能性があります。この問題を解決するための方法を試していこう、ということですね。
具体的な方法として、ネームとテキストをキャプチャーするというアイデアについて話しています。キャプチャーリストを利用することで、特定の変数をクロージャ内にキャプチャすることができるので、このリストに = self.name
のように書くことが提案されています。これは、自己参照の仕組みを簡単にするトリックです。
キャプチャーリストを正しく使用しないと、プロパティを無限に参照し続けてしまうことがあります。そのため、オプショナルを使わないことが一つの選択肢としてあげられています。そして、var
よりも let
を使うことでより安全にする話にもふれています。
さらには、lazy var
を使って遅延初期化を行うことも紹介されています。これは、変数が実際に必要になったときに初めてメモリを割り当てることで、不必要なキャプチャーを防ぐという利点があります。
結局、この方法によって、共参照循環の問題を検証する際には便利であるため、開放タイミングをプログラマーが管理することで改善が図れると話しています。
最後に、HTMLに関係するコードについても触れていますが、ここでは HTML の初期化式を回避し、不要なセルフキャプチャーを防ぐための方法を試しています。これは、print
などを使って実験的に検証しているようです。全体として、プログラムが正しくメモリ解放できるように工夫することが大事だということが示されています。 これからセルフのキャプチャーや循環参照の解消についてお話しします。この方法はスライドでは紹介していない方法で、そもそも循環参照をさせないというものです。その方が賢いと個人的には感じます。
実際にコードを書きながらやっていきたいと思います。前にスライドで解消方法を紹介しましたが、次回にまた詳しく話しますので、興味を持っていただけたらと思います。先ほど、スコープを作って解消させましたが、それをスコープを作らずにできるのか、オーナーシップについて試してみたいところです。
次に、HTMLエレメント
プラスを使ってみます。これをlet
で書くことができるんですよね。Consumable
なパラグラフを動かそうとしてエラーが出てしまいました。コンパイラからエラーが出るとどうしようもないですね。しかし、本当に解放できるかと思ったのですが、クラスではconsume
しても継続して使える場合があるんです。
では、function
でsomething
を試してみましょう。例えば、T
でHTMLエレメント
をコンシューミングして持たせると渡したときに消費するはずです。しかし、something
に一度渡すと、その後も渡せてしまうんですよね。プリントで名前を表示しようとしても、消費されずに残っています。これが正しい動きなのかは頭の中で整理中ですが、ストラップにするとちゃんと消費してくれるのに。
もしかしたら、lazy var
だからmutating
が関係しているのかもしれません。この辺りはややこしいですね。Mutating self-escaping closure
ではキャプチャできないということも初めて知りました。なので、mutating
キャプチャリストでどうにかしなければならないのでしょうか。でも、ストラップだと消費するのに、プラスだとコンシューミングに渡しても消費しないという現象があるようです。
コンパイラバグと言われても困るのですが、この辺りも含め、次回は実際の循環参照の解消を見ていこうと思っています。それでは今回はこのくらいにしましょう。お疲れ様でした。ありがとうございました。