今回は、前回に眺めたクロージャーにおける キャプチャーリスト
を使って クロージャー
における 強参照循環
の解消方法を見ていきますね。よろしくお願いします。
——————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #314
00:00 Start 01:34 キャプチャーリスト 02:11 参照方法を表す仕組み 04:14 弱参照と無所有参照 04:54 インスタンスのライフタイムで考える 09:20 弱参照はオプショナル型 10:08 nil になる可能性があるかを意識するのは重要 11:38 強参照循環を解消していく 17:38 強参照循環解消の様子をコードから読み解く 21:01 unowned とするか、weak とするか 24:08 強参照循環の解消についてのおさらい 26:00 使いまわし方を間違えれば unowned でクラッシュする 29:13 並行処理と生存期間 31:39 コードが同じでも扱い方で変わってくる 32:50 クロージング ———————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #314
キャプチャリストについては注意が必要です。特に循環参照の解消を意識する際には、キャプチャリストを使うことで参照の持ち方を調整することができます。キャプチャリストでは、角括弧で囲った中にキャプチャしたいものを指定します。単にインスタンス名を書くことで、それをキャプチャしますし、値を書くことでその値をキャプチャすることもできます。
特に「weak」や「unowned」の指定ができる点が重要です。これは所有権管理の観点からも興味深い部分です。Swift 5.9からはオーナーシップの概念が導入され、所有権というものがより明確に扱われるようになりました。しかし、「unowned」と所有権の間には直接的な関係はありません。ここでの「unowned」は所有しない参照のことです。
ウィーク参照やアンオウンド参照という言葉はあまり日常的には聞きませんが、プラットフォームによっては無所有参照や委譲参照と呼ばれることもあります。キャプチャリストにおいて、これらの参照方法を指定することで循環参照を解消することができるのです。
また、クロージャーとキャプチャしたインスタンスが密接に関連している場合、つまり、これらが常に同時に解放される時は「unowned」参照として定義することで効率とパフォーマンスが良くなります。しかし、同時に解放されない可能性がある場合や、メモリーの安全性を考慮する必要がある場合には「weak」参照を使うことが推奨されます。この場合、「nil」になる可能性があることを認識しながら利用する必要があります。
練習を行う際には、「weak」に依存し過ぎないように注意しつつ、キャプチャリストの参照がいつ「nil」になるかわからない場合には「weak」参照として定義することが安全です。このことを理解して、効率的にキャプチャリストを使いこなして行くことが大切です。 実際にプログラミングを行う際には、コード内でどのタイミングでnil
が発生するか不明な場合があります。これはプログラムのロジック上、外部要因によって異なることがあります。そういった状況では、nil
がどこで発生するかが確定していないことを意識しながらコードを書いていく必要があります。
弱参照
や無償有参照
、さらには参照全般についての知識をしっかりと身につけることが大事です。特に、特定のタイミングでnil
が確実に発生するかどうかを理解することは重要です。場合によって弱参照
を使うと良いですが、確実にnil
にならないと分かっている場合には無償有参照
を選択するのが理想的です。
弱参照
は常にオプショナル型となり、nil
になる可能性があるため、オプショナル型を用いることが言語仕様によって求められます。このような設計の基礎にあるのがARC(Automatic Reference Counting)であり、クロージャ内で参照先の存在性を確認できる仕組みです。ランタイムでインスタンスが生存しているかどうかを確認するために、nil
になっているかどうかをチェックするという話もあります。弱参照
のメリットでもあります。
キャプチャした参照がnil
になる可能性がないのであれば、無償有参照
でキャプチャすべきですが、これは他のプログラム言語でも議論されるところです。nil
を想定する必要がないときは、そのまま論理を進めたほうが効率的です。逆に、nil
の判定をしても予期しない結果になることもあるため、なぜnil
になるのかを理解したうえで、その後の処理を考えることが重要です。
以前に紹介したサンプルコードで、無償有参照
を利用する方法が示されています。例えば、クラス内で自身のプロパティをキャプチャして循環参照が発生するときは、キャプチャリストを使用し、self
を無償有参照
として指定します。これによって、循環参照を断ち切ることができ、クラスインスタンスとクロージャの双方が正しく解放されますが、この手法は慣れないと難しいかもしれません。
HTML要素のクラス
を例に考えてみましょう。プロパティに自身をキャプチャするコードで循環参照が起きるとき、キャプチャリストにself
を無償有参照
として記載することで問題を解決します。たとえば、GitHub
などで他の人のソースコードをレビューする際に、キャプチャしていることが不明瞭なケースもあります。このような状況では、self
を使っていることを強調するために、自分で確認しつつコードを理解することがおすすめです。 なので、このセルフは必ず記述しましょうということになりますね。今のところ、このセルフを解消するための手段を提供していきたいと考えています。HTMLのエレメントがありますが、これが非常に便利です。
オプショナルについてですが、新しいストラップになったため、これを押すと合っていると思います。しかし、イントパンという部分で少し引っかかるかもしれません。共通する瞬間があると、このままでは解放されません。実行してみても結果が表示されないだけかもしれません。
ここでもう少し手を加えて、要素が解放されるかどうか確認できると、より分かりやすくなるでしょうね。ビルドがちゃんと動いているかどうか確認していきたいと思いますが、ビルドマッチのエラーがあるなら仕方がないですね。
次にどこにデータを出すかを決定し、この部分にアクチャリストを入れて解放を実際に行うか確認していきます。もうすぐ動くようになると思いますが、久しぶりにノートパソコンを使っているので、昔は遅かったと感じますね。インテルのGPUでは、重たい処理を同時に行うと動作がかなり遅くなってしまいますが、M1に変更してからは非常に快適になりました。
とりあえず、動作は確認できましたが、解放されていないようです。抵抗がある場合には、こういうやり方で動かして、解放されるようにするべきです。これで瞬間参照が解消できたことが確認できます。
結果がどうであったとしても、ライフサイクルに関する部分をしっかりと繰り返して確認していくことが重要です。まず、ここで変数にHTMLエレメントを入れたので、これは共参照になります。そして、26行目でこの共参照が解放されるため、これで一つのセットが完成します。
その後、パラグラフのプロパティである asHTML
を参照するとき、これがすでに初期化されていた場合、そのインスタンスに独自に値がセットされていなければ、デフォルト値が使われます。今回は特にセットしていないので、この参照したタイミングで初期化式が動きます。ここでクロージャーがプロパティに代入されるという流れですね。 なので、ここで代入されたときにクロージャがプロパティに共有参照されます。何もキャプチャリストを指定していない場合、この中でキャプチャされている self
も同様に共有参照されます。しかし、今回の場合はアンオウンドセルフにしているので、ここでは無所有参照として保持されるクロージャが存在します。これによって、self
は共有参照されないわけです。
ここで、代入初期化のときは共有参照ですが、セルフは無所有参照になるため、参照カウントは1になります。そして、その参照カウントが解放されると、イニシャライザが動くという流れになります。具体的な動きとしては、共有参照が1から0になったのでイニシャライザを呼びます。イニシャライザは特に何も行わず、終了したタイミングでほとんどのプロパティも解消されるので、name
や text
、さらに html
も解放されます。これが解放されるということは、保持していたクロージャが解放され、その中にキャプチャされていた self
も解放されます。解放と言っても無所有参照なので、単純に参照がなくなるという動きになります。これによって、全体が何も残らずに問題なく動作し、解放も無事にできたわけです。
次に、アンオウンドが良いのか、ウィークが良いのかを考えてみます。スライドに戻ると、クロージャとそれをキャプチャするインスタンス、つまり今回の場合では html
というクロージャと、それをキャプチャする self
が互いを常に参照していて、同時に解放されるなら無所有参照が適切だということです。これが条件に一致しますね。
つまり、html
と self
はプロパティを参照するタイミングでクロージャが self
をキャプチャし、クロージャが保持され、self
がクロージャを保持するという形です。そして、解放されるときに、終了処理のフェーズでクロージャが解放され、self
が破棄されます。これが同時に発生するので、フェーズの観点から見ても無所有参照が最適になります。
将来的にどこかで nil になる可能性がある場合には、弱参照が適切かもしれませんが、今回の場合、self
が無くなることは全くありません。クロージャが self
に所有されている、つまり代入されているので、self
が無くなったときには、html
も無くなるべきだからです。そのため、弱参照である必要はありません。
この理屈により、無所有参照が最適ということになります。基本的にこれでOKです。次のスライドに進むと、無所有参照が最適な方法について説明した部分があります。解消する手段としては、キャプチャリストを追加しただけで、無所有参照の問題も解消できます。具体的には、キャプチャリストで self
を無所有参照としてキャプチャするという意味になります。これで順番に問題が解消されます。
次回は別のセクションで、演算子などを確認していくことになるかと思います。ただ、今回の内容を振り返ると、常にお互いに参照し、同時に解放されるので無所有参照が最適と考えましたが、HTMLを参照しない場合もあります。例えば、html
が未初期化の段階では、paragraph
が self
をキャプチャしていない状況が考えられます。 ですので、同時に処理が進んでいても、お互いが常に参照し合っているわけではないです。これ自体は特に問題はないですし、クローズ処理に移ることはありませんから、全く問題になりません。しかし、問題が発生するのは、実際にクローズを読んで、変数が参照されている場合です。例えば、無償で参照されているとしましょう。文字を取得して2回表示させれば、特に問題はありません。表示後、開放されて処理は終了します。
ただし、もしnilを渡して開放することになった場合も、問題はありません。しかし、別の場所でクローズを参照してしまい、開放後にそのセルフを復元しようとすると、エラーが発生する可能性があります。このような状況では、非同期的に処理が行われるため、それがどこかで処理されてしまうと、予想外の動作となることがあります。
一方で、非同期処理(async)で渡す場合、処理が実行されるタイミングが不明瞭になるため、セルフが既に廃棄されてしまう可能性があります。こうした場合には、クロージャを使う際に、変数がまだ存在しているかどうかを確認する必要があります。こうすることで、生成されたコードの使い方によって、開放されるタイミングや生存範囲が変わることがないようにできます。結局、予測が難しい場合が多いため、一般的には「weak」にしておくのが適切だと思われます。
今回の例では、同時に開放されることが保証できないため、弱参照(weak)の使用が適切だと考えられます。プログラミング言語が提供する機能に対して、必ずしも適切な対応がなされているとは限らないため、少し慎重に考えてみて欲しいと思います。
以上のようなわけで、少し難しく感じるかもしれませんが、確実性を高めるためにweakを使用するのも理解できるところですね。ただ、絶対に大丈夫なケースもありますので、そういう場面ではシンプルにコールバックを利用するのもありです。そういった気持ちで、このキャプチャー機能に向き合うとよいのではないかと思います。
では、時間になりましたので、今日はこれで終了します。お疲れ様でした。ありがとうございました。