さてさて今日からは 循環参照
問題のもうひとつの要因として挙げられる クロージャー
と クラスインスタンス
間における話題について見ていきます。なんとなくまたサンプルコードに微妙な話題性が詰まっている気がしなくもないですけれど、そんなところも楽しみつつ眺められたらいいなと思ってます。よろしくお願いしますね。
——————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #252
00:00 開始 00:21 強参照循環という表現について 02:35 循環参照とは? 04:12 循環参照はなぜダメなのか? 05:19 相互の立ち位置が不明瞭になりそう 06:08 テンプレート的なインスタンスを扱う場面を考える 07:28 マスター情報の変更を伝搬させるときの循環問題 09:48 強参照循環は循環参照のうちのひとつ 10:27 手に負えないくらいの複雑な参照問題 11:29 循環参照のリスクを踏まえて扱っていく 11:57 インスタンスの開放問題にフォーカスしたいなら、"強参照循環" 13:17 クロージャーにおけるインスタンスの循環参照 13:52 クロージャーにおける強参照循環が起こる場面 15:22 クロージャー内でメソッドを呼び出すと、その所有者をキャプチャーする 19:39 クロージャーを挟むと強参照循環が見えにくい印象 20:42 クロージング ———————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #252
はい、では始めていきましょう。今日は「循環参照とクロージャ」についてです。確かに、クロージャの中で循環参照が起こり得ますよね。
「循環参照」って何度も言っていますが、あえてこの勉強会でも繰り返してきました。しかし、その結果として「循環参照」という言葉が意識に染み付いてしまい、逆に「参照循環」という表現が出にくくなってしまいました。APIデザインガイドライン的には、自分の中でお馴染みの言葉を使うよりも、一般的に認知されている言葉を使う方が良いとされています。リストはリスト、配列はアレイという具合に、一般的に使われる用語を選ぶことが重要です。学習者が言葉の意味を調べる際、一般的な用語の方が情報を探しやすいからです。
実際、循環参照について調べてみると、プログラミングにおける循環参照問題が目に付きます。Excelでも循環参照という言葉が使われていて、その問題が指摘されています。なぜなら、循環参照は問題とされるべき現象です。
循環参照の問題は、インスタンスが解放されなくなり、メモリが逼迫することにあります。これ自体は直接的な問題ではないかもしれませんが、予測できない問題が発生します。例えば、通常は正しく後始末されるべきオブジェクトが、循環参照のせいでメモリに残り続け、予期せぬ動作を引き起こすことが問題です。これに加えて、どちらのオブジェクトが管理されるべきかが曖昧になるなどの問題も生じます。
この勉強会の中では、アイテムクラスを使った具体的な例についても話しました。アイテムオブジェクトは複雑なので、たくさんのデータを持っている場合、その初期化が大変です。そのため、コピーコンストラクタやアイテムマスターを使って、テンプレートからコピーする方法も有効です。
以上のように、循環参照がなぜ問題かや、その対策方法について深く掘り下げて理解することが重要です。特にクロージャを使用する場合、注意が必要となります。引き続き、他の問題や具体的な例も確認しながら学習を進めていきましょう。 アイテムマスターという概念が爆誕したときに、そのテンプレートクラスという言い方は少し誤解を招くかもしれませんね。C++で言うテンプレートクラスを作成するように、クラスをテンプレートとして表現し、そのテンプレートをコピーして使いたいという発想です。つまり、何か肩開けをするのなら問題ありません。
他にも複雑なケースがいろいろあります。たとえば、ジオバのアイテムを良い状態でコピーして作りたい場合、その時にはジオバマスターとしてマスターID値を作って、そこから後の部分を作成していく、といった形です。アイテムクラスが存在し、ユーザーがアイテムからコピーしてアイテムオブジェクトを作りたい場合、そのアイテムオブジェクトは複雑です。制作者にはかなりの恐怖をもたらすでしょうね。これだけの量を読んだら、イニシャライザーを飛ばしてしまいたくなるかもしれません。
ですが、イニシャライザーは必要です。しかし、コピーコンストラクター的なものをアイテムマスターに置き換え、その上でテンプレートを使用します。C++のようにテンプレートクラスを作り、それをコピーして使用するのです。類似した複雑なケースも存在するので、ジオバのアイテムを良い状態でコピーする方法について考え、その際にジオバマスターとしてマスターID値を作成し、続けてウーパン以降を作っていくという流れになります。
ジオバのアイテムプロパティにちょっとした変更を加えた場合、次に述べるようにします。アイテムマスター1を作成した場合、それを基にしてマスターを作り、そのマスターに変更を加えるのが大変なら、マスターが更新された際に、マスターから作られたアイテムがこれらの変更を反映するようにします。問題がここに出ますので、ちょっと独立した方が安全かもしれません。
次に、アイテムマスター1を基にしたマスターのプロパティに変更を加えたときに、変更を反映させるのが難しい場合があります。アイテムを作成する際には、マスターID値を基にしてウーパン以降を続けて作成していくので、マスターに一度変更を加えた後、その変更が新しいアイテムに反映されるようにするのは手間がかかります。
ここで無限ループの問題が発生するかもしれません。参照が複雑になると、無限ループが発生してしまう可能性があります。循環参照の問題に気をつけなければならないですね。特に、誰がマスターになるべきか、あるいは誰が管理すべきかといった点で類似の問題が発生することがあります。
場合によっては、アイテムマスターを更新して、それに基づいてアイテムコピーを作成する必要があるかもしれません。次のアクションについて考えると、アイテムマスターをどうにか更新するというのは面白い課題です。
読み進めるのが疲れてきたので、ここで少し飛ばして、もう一度確認してみます。循環参照の問題に戻ると、無限ループが発生する可能性があるという事です。参照が複雑になると、無限循環が発生するリスクが高くなります。この問題は「誰がマスターになるべきか」や「誰が管理すべきか」という問題と似ているかもしれません。 問題2としては、ここまで読んでいて思ったのですが、先ほど説明したのは循環参照についてでした。ただ、どちらかというと共参照の循環の方の問題点に意識が寄りすぎていたかもしれません。純粋に循環参照の問題としてこれが挙げられていますね。いい感じです。
前回の話では、どちらかというと共参照についての説明がちゃんとできていたと思います。2番目の問題は、またマスターの話ですね。どうやって13番をマスターにしようと考えたときに、この時点で13番のアイテムを元にマスター2を作成し、そこから14番、15番を作成するという流れになります。13番のアイテムはID1から作成されています。参照が複雑になりますね。この状態でアイテムマスター1から作られたものを変更したときに、その変更がどこまで反映されるかという問題もあります。
ただ、これは少し複雑すぎて、循環参照の問題と重なっている部分があります。リスクを理解した上で対処することが重要です。このリスクを知った上で扱うことが大事だという話ですね。ただ、今回の例は複雑で少し恐ろしすぎたかもしれませんが、こういった情報が出てきました。
循環参照に関する問題が解消されるかどうかに焦点が寄りすぎていたことに気づきました。循環参照の問題を調べると、やはり共参照循環の話が関連してきますね。循環参照の理解の助けに共参照循環の方が向いている気がします。循環参照の中でも共参照における問題が見られるため、共参照循環という言葉を使ってみましょう。問題が起きたらまた考えます。
クロージャーにおいても循環参照に関連する問題があります。たとえば、クロージャーが循環参照を引き起こすと、いろいろと不具合が生じることがあります。これも共参照循環にフォーカスがかかったものですね。
前回の話では、2つのクラスのインスタンスのプロパティが共参照を形成したときに何が起こるかを見て、その解消方法についても学びました。共参照と無所有参照の違いを学び、これまで見てきた共参照循環は、クロージャーをクラスインスタンスのプロパティに割り当てた際に発生します。そのクロージャーの中でクラスインスタンスをキャプチャーすると、クロージャーは参照型であるため、そのクラスインスタンスが共参照を持つ状態になります。インスタンスがそれを持つクロージャーをさらに持つと、共参照関係を形成するということになります。
以上が今までの話の要点です。少し難しい部分もありますが、理解が深まると面白いですね。 とりあえず、このキャプチャーについて説明します。クロージャー内でself
をキャプチャーする場合、self
のプロパティやメソッドも一緒にキャプチャーされるため、それが原因で循環参照が発生する可能性があります。具体的な例を見ていきましょう。
例えば、以下のようなクラスがあります。
class SomeClass {
var f: (() -> Void)?
}
このクラスを利用してインスタンスを作成し、そのf
プロパティにクロージャーを設定します。
let instance = SomeClass()
instance.f = {
print("self: \\(instance)")
}
ここで、instance.f
に設定したクロージャー内でinstance
をキャプチャーしています。この場合、SomeClass
のインスタンスがクロージャー内でキャプチャーされています。結果として、SomeClass
インスタンスとクロージャーが相互に参照し合うため、循環参照が発生します。
循環参照の影響としては、そのインスタンスが解放されなくなる、つまりメモリリークが発生するといった状況が考えられます。例えば、以下のコードを考えます。
instance.f = {
print("self: \\(instance)")
// ここに他の処理
}
この状態だと、instance
が解放されないため、プログラムの実行が進んでもインスタンスはメモリに残ったままになります。これを防ぐために、クロージャー内でweak self
を使うことが一般的です。以下のように書き換えます。
instance.f = { [weak instance] in
guard let instance = instance else { return }
print("self: \\(instance)")
}
これにより、instance
は弱参照されるため、循環参照を防ぐことができます。
循環参照は、理解が少し複雑で実際のコードで問題が発生するまで見逃しがちです。循環参照が心配な場合には、定期的なチェックや適切なリファクタリングによって対処することが大切です。
今日はこの辺りで終わりにしましょう。お疲れ様でした。ありがとうございました。