今日は再び 循環参照
の話題に戻って、そこで起こりうる問題点とその解消方法周りを見ていきますね。いつものようについてまわるお馴染みな機能のところですので、この機に初心に帰って今だから見つけられる要所がないか探してみるのも面白いかもしれないです。どうぞよろしくお願いしますね。
—————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #219
00:00 開始 01:25 強参照循環のおさらい 03:40 共有ではインスタンスの管理が大切 04:32 循環参照ってどういう状況? 07:49 強参照で循環したときが問題 08:45 メモリーリーク 09:50 解放処理が呼ばれないことが重大 11:49 終了処理が実行されないことについて 20:14 ポインターも参照型特有の難しさ 21:03 解放処理を deinit に書いて、呼ばれない例 22:56 強参照循環を解消する上でのキーワード 23:44 クロージャーによる循環参照も高難度 25:15 確保されなければ循環参照しない 27:13 インスタンスの所有関係 30:28 全て構造体でやってみるみたいな発想のしかたも 30:58 相互依存は最低限度に 31:40 クロージングと次回の展望 ——————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #219
さて、勉強会を始めていきましょう。今日は「循環参照」について取り上げます。なかなか難しいテーマですね。昔はこの辺りは自動化されていなかったので、嫌でも勉強する必要がありましたが、最近では自動化が進んで表から見えにくくなったため、改めて難しさを感じることもあります。この勉強会でもその点を再認識しましたね。コードを組む際に低レベルな部分が隠蔽され、難易度が上がることもあります。具体的に問題を感じ取れるものの方が比較的容易でしょう。
循環参照についてですが、これはクラスのインスタンスにおいて生じる問題です。なぜクラスで起こるかというと、参照型という特性が大きく関与しています。今回はおさらいですので、ざっと見ていきましょう。分かりにくい部分や興味があるところがあれば、どんどん聞いてください。
では、循環参照がなぜクラスで起こるのかについて再確認します。クラスは参照型として扱われ、一つのインスタンスがメモリ上の1ブロックを複数箇所で共有することができます。これが参照型です。それに対して値型というのもありますが、値型は基本的に一箇所からしか参照されません。この参照型という特性が循環参照の問題を引き起こします。
具体例として、テレビの電源管理を考えます。誰かがテレビを見ている間は電源が入っていますが、誰も見なくなったら電源を切ります。録画予約がある場合も、予約が終わったら電源を落とす必要があります。リソースの使用状況を把握し、使われていないと判断したら解放するというのが、ARC(Automatic Reference Counting)の役割です。
ARCの基本的な考え方は、参照カウントを管理してリソースを解放するタイミングを決定することです。たとえば、妹がテレビの録画予約をしてから出かけた場合、その間はテレビが使われている状態です。このように、参照が持続することを前提に考えます。
さて、具体的なコード例に戻ります。たとえば、Person
クラスのインスタンスが生成され、それに関連してApartment
クラスのインスタンスが生成された場合、次のような状態になります。
class Person {
var name: String
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("\\(name) is being deinitialized")
}
}
class Apartment {
var unit: String
weak var tenant: Person?
init(unit: String) {
self.unit = unit
}
deinit {
print("Apartment \\(unit) is being deinitialized")
}
}
var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
この例では、Person
とApartment
が相互に参照しているため、循環参照が発生します。この場合、参照カウントが循環してしまうため、お互いのインスタンスが解放されません。
このような循環参照を防ぐために、Apartment
のtenant
プロパティをweak
(弱参照)として定義しています。これにより、Person
インスタンスの参照カウントが増加せず、適切に解放されるようになります。
これが循環参照を回避する基本的な方法です。他にもunowned
というキーワードを利用する場合もありますが、それはまた次回に紹介します。以上が基本的なおさらいとなります。それでは、引き続き質問や疑問があればどうぞ。 ある変数がインスタンスを参照している状況を考えます。例えば、ジョン
という変数がパーソン
のインスタンスを参照しており、ユニット4a
はアパートメント
というインスタンスを参照している、それぞれ一箇所ずつから参照している状態です。この状態では特に問題はありません。
次に、一つのクラスのパラメーターにもう一つのクラスを入れ、そのクラスのパラメーターにも最初のクラスの値を入れると、循環参照が発生します。強い参照(ストロング参照)が一箇所からもう一箇所へ、さらにその隣からまた戻ってくるといった具合に、循環する形になります。これが循環参照です。
循環参照自体は特に問題がないように思えますが、問題が発生するのは解放するときです。例えば、ジョン
やユニット4a
を使わなくなったとき、一方から見るともう一方はまだ使われている状態になっています。プログラムから見ると、変数を通さないとインスタンスにアクセスできないにも関わらず、参照が残っているのでこれらのインスタンスが解放されません。
これがメモリーリークとしての問題です。メモリーを数回漏らす程度なら問題ないかもしれませんが、数百回のループが繰り返されると大きなメモリーリークが発生します。このため、循環参照はしっかりと管理して解消しなければなりません。
コメントでも指摘がありましたが、最近ではメモリーリーク自体よりも、本来なら解放されるべきメモリが解放されないことで、データがクリアされずに予期しない動作を引き起こす問題が注目されています。
昔、自分が初めて使ったパソコンはメモリーが4キロバイトしかありませんでした。その当時の感覚では、メモリーリークは非常に深刻な問題でした。しかし現在では、16GBのメモリーを持つコンピュータが一般的であり、小さなインスタンスのメモリーリークはあまり問題にならないことが多いです。それでも、メモリー管理は常に重要な課題です。 確かに、そこを気にしないといけないですね。例えば、「プラスA」というのがあって、まあ、この「プラスA」について考えます。具体的な例を作ろうとすると少し難しいので、抽象的に説明しますね。
「プラスA」にはファイルハンドルの操作があります。実際に Foundation
をインポートしないとファイルハンドルは使えませんが、この例を仮に話します。ファイルハンドルを用意して、それをイニシャライザーで初期化します。そして、ファイルハンドルに対してデータを書き込むメソッドを呼ぶとしましょう。ファイルに書き込んだデータは、ファイルハンドルをクローズするまで実際にはファイルに反映されません。
このため、ファイルハンドルをクラスで管理する場合、ハンドルの解放が必須です。クラスは参照型であるため、意図的に解放しないとメモリリークが発生する恐れがあります。例えば、複数のファイルを順番に処理する際に、各ファイルをシーケンシャルに処理していくとしましょう。そのシステムが途中でクラッシュした場合、データがちゃんと書き込まれていないことがあります。
次のようなコードを考えてみます。最初のファイルを firstFile
として、次に nextFile
を設定します。それを繰り返し、すべてのファイルをシーケンシャルに処理していきます。ここで、例えば最初のファイル firstFile
の nextFile
は secondFile
ですが、その nextFile
が何らかの事情で循環参照を起こすと、大変な問題が発生します。
class FileHandler {
var nextFile: FileHandler?
init() {}
func open() {}
func write(data: Data) {}
func close() {}
}
このようにして、ファイルが全て不要になったとき、optional
を使って FileHandler
クラスのインスタンスをスコープ内で操作します。関数の中でファイルにいろいろ書き込み、最後にスコープを抜けると自動的に解放されるはずですが、循環参照がある場合は解放されません。
また、これはファイルハンドルに限ったことではありません。データベースでコミットが行われないとデータが反映されない問題や、ユーザーインターフェイスの設定画面で設定項目を変更しても反映されないケースなども考えられます。
このような循環参照やリソースの解放は、油断すると大きな問題になりかねませんので、しっかり管理することが必要です。 循環参照と解放について見ていきますが、それだけでは対処しきれないことがたくさんあります。やはり、基礎としてポインターなどがどのように影響しているかを理解することが重要です。この部分を理解することがステップアップするための必須項目かもしれません。
情報処理技術者試験、現在の「基本情報技術者試験」でもポインターは出題されるかもしれません。以前の「第2種情報処理技術者試験」でも出ていた重要な概念です。
それから、実際のアプリ制作において循環参照をどのように防ぐかについても考えていきます。次の今後のスライドで触れる予定です。簡単にキーワードだけ挙げると「弱参照(ウィーク)」を使って解消します。詳細については次回以降になりますので、都合が良ければ来てください。都合が悪かった場合は自分で「弱参照」の内容を調べてみてください。
また、コメントで触れられていた「エスケーピングクロージャー」についても、実際に残ってくる問題ですね。これも重要なトピックです。 次に紹介するのは、クラスのプロパティに対しての循環参照の解説です。一般的には、クロージャーのキャプチャーで循環参照が問題になることが多いですね。クラスがなくても構いませんが、ここでは関数とクロージャーの関係で見ていきましょう。普通のコールバックの例で説明します。
クロージャーを受け取ってアクションでコールバックする関数があるとします。このとき、循環参照を防ぐためにどのようにするかが課題となります。関数にクラスのインスタンスを持たせる方法が分かりやすいかもしれませんが、その際に所有関係をどう保つかが問題になります。
例えば、ディスパッチキュー(DispatchQueue
)を使う場合、バックグラウンド実行 (async
) で呼び出されるクロージャーがインスタンスをキャプチャする状況があります。このとき、クロージャー内でインスタンスへの参照を弱参照 (weak
) にするかどうかを考える必要があります。エイシンクで投げた場合、このクロージャーがディスパッチキューにキューされると、インスタンスも一緒に保たれることになりますが、キューが解消されるとクロージャーも解放され、インスタンスも解放されます。
ここで、冗長に見えるかもしれませんが、カプセル化しておくことで安全性が高まる場合があります。特に理解が不十分な場合は、安全策を取ることが推奨されます。所有関係について詳しく考え、循環参照を避けるための意識が重要です。
具体的なアプリケーション開発では、エスケープクロージャーでデッドロックや循環参照が発生する可能性があるため、これらのキーワードを調査し、理解することが基本です。所有権は常に一方的で、上流モジュールが下流モジュールを所有する形になります。これを理解しておくと、データレースやその他の競合のリスクを減らすことができます。
クラスを読み取り専用にした場合でも、参照カウントは維持されるため、解放されない可能性があります。ストラクトを使うことでデリゲートを使う必要がなくなる場合もあり、「まずストラクトを試してみて、どうしようもなくなったらクラスを使う」という順番も一つの考え方です。
次回は、循環参照を解消するためのオーナーシップ周りの考察をさらに深め、未所有参照(unowned
)についても触れていきます。これらのポイントを抑えることで、安全で効率的なコードを書く手助けになります。
本日はここまでとします。お疲れさまでした。また次回お会いしましょう。ありがとうございました。