前回は、迷宮に自ら入り込んだみたいに 参照カウンター
から目標なしにいろいろ眺めていく回で、なかなか高難度な出来事に出会ったりしたのが印象に残りましたけれど、今回は再び基本に帰って The Swift Programming Language の 循環参照
まわりのところから眺めていく感じにしてみますね。よろしくお願いします。
—————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #214
00:00 開始 00:26 今日は循環参照の話 01:47 前回のおさらい 03:33 強参照循環の状態 06:18 強参照が 0 にならないコード 11:32 強参照循環の起こる仕組み 12:32 片方が解放されれば循環は崩れる 15:34 解放のタイミング 16:26 終了処理を手動実行させる手法も 18:46 弱参照を使わないで強参照循環を解消するとしたら? 20:47 インスタンスを管理する仕組みを作る方法も? 24:00 スマートポインター的な考え方 27:08 クロージングと次回の展望 ——————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #214
では、話を進めていきましょう。今日は「強参照の順番」として、Swift Programming Languageの本に基づいて説明を進めていきます。特にARC(Automatic Reference Counting)のセクションに焦点を当てます。
まず、強参照順番についてですが、英語では「ストロングリファレンスサイクル」と表現しますね。これは循環参照の一種です。日本語では「順番参照」という表現がされる場合もありますが、ここでは「強参照順番」という用語を使います。
強参照順番について具体的に説明します。ARCでは、インスタンスの参照カウントを管理し、参照カウントが0になったタイミングでメモリを解放します。そのため、循環参照が発生すると参照カウントが0にならず、メモリが解放されないという問題が起こります。
例えば、次のような場合です:
class Person {
var name: String
var partner: Person?
init(name: String) {
self.name = name
}
deinit {
print("\\(name) is being deinitialized")
}
}
このようなクラスがあるとします。ここで、強参照による循環参照が発生すると、デインシャライザが呼ばれません。
var person1: Person? = Person(name: "Alice")
var person2: Person? = Person(name: "Bob")
person1!.partner = person2
person2!.partner = person1
person1 = nil
person2 = nil
このコードでは、person1
とperson2
がお互いを強参照しています。そのため、どちらの参照も解放されず、メモリリークが発生します。
これを解消するためには、弱参照(weak
)やアンオウンド参照(unowned
)を使用します。
class Person {
var name: String
weak var partner: Person?
init(name: String) {
self.name = name
}
deinit {
print("\\(name) is being deinitialized")
}
}
var person1: Person? = Person(name: "Alice")
var person2: Person? = Person(name: "Bob")
person1!.partner = person2
person2!.partner = person1
person1 = nil
person2 = nil
これで、person1
とperson2
の参照が nil になったとき、両方のインスタンスが解放されます。
ARCと循環参照に関連した具体的な例として、二つのクラスAとBが互いに強い参照を保持する場合などがあります。クラスAがクラスBのインスタンスを持ち、クラスBがクラスAのインスタンスを持つという状況です。
class ClassA {
var instanceB: ClassB?
deinit {
print("ClassA is being deinitialized")
}
}
class ClassB {
weak var instanceA: ClassA?
deinit {
print("ClassB is being deinitialized")
}
}
var a: ClassA? = ClassA()
var b: ClassB? = ClassB()
a!.instanceB = b
b!.instanceA = a
a = nil
b = nil
このようにして、弱参照を使用することで、循環参照を防ぎ、メモリリークを回避できます。
今回の話題は、SwiftやObjective-Cなどの言語における典型的な問題解決に直結しており、他のガーベジコレクションを持つ言語(例えばC#やJava)とは異なる注意が必要です。
最終的には、ARCの仕組みを理解し、強参照と弱参照の使い分けを理解することが重要です。これにより、メモリ管理の問題を効果的に回避できるようになります。 インスタンスが両方とも解放されました。しかし、B
に対してA
をアサインすると、解放されなくなる場合があります。これは瞬間的に内部で参照が発生して、参照カウントがゼロにならないコードが書かれているためです。ARC(自動参照カウント)が解放するのを先送りし、そのままプログラムが終了してしまったことで、解放されずにプログラムが終了した状態です。 無くなるべきインスタンスが無くならない状況になってしまったわけです。
どうして参照がゼロにならないコードが書けたのかというと、A
のインスタンスとB
のインスタンスが相互に強参照を持っているからです。この状態でA
をB
に渡すと、B
がA
を強参照し、同様にA
のプロパティB
もB
を強参照します。この相互参照のために、他に工夫が必要となります。どちらか片方が解放されると状況は変わるかもしれませんが、解放されるタイミングが現在の状態では発生しません。
例えば、どちらかのデイニシャライザが呼ばれたときに解放が起こると考えられますが、現状ではそれが発生していません。よって、A
がB
を参照し、B
がA
を参照することで、お互いが参照し合う循環が生まれています。この循環参照により、メモリリークが発生するのです。
対策としては、例えば、B
のA
に対してnil
を設定して片方を解放する方法があります。これは、A
がB
を見ている状況ですが、B
は何も見ていない状態にすることです。循環参照がなくなるため、A
はもう解放できるようになり、A
が解放されるとB
も解放されるという順序になります。実際に、このような操作を行うと、解放の順番も変わることが確認できます。 まず、ABの同じタイミングでアサインして、先にBのメモリを解放すると、結局ABの順番で解放される、という状況になります。解放のタイミングについてですが、Aが解放されて、次にBが解放される形です。このとき、print(1)
を呼び出して、nil
を入れたタイミングで解放されます。しかし、上の参照が残っているので変わらないのです。また、参照が減っただけで、この共参照がずっとストック内で有効なため、最終的に一括で解放される感じです。
これが共参照の順番です。ARC(Automatic Reference Counting)で管理されていなかった頃は、例えば参照の順番が崩れると厄介なことになるので、明確に自分でリリースメソッドを呼んで後始末をするコードを書いていた時代もありました。他のプログラミング言語では、例えばオーディオやグラフィック関連のインスタンスを無効化するためにinvalidate
を呼ぶようなことが見られたりします。
AとBを使用した後で、invalidate
を呼んで無効化することも可能です。ただし、invalidate
みたいなメソッドを自分で実装すると、それを呼び忘れた場合にメモリリークやファイルハンドルの問題が発生するリスクがあります。こういった問題に対処するために、プログラマーが義務を背負うことなく安定したコードが書けるように、デイニシャライザー(deinitializer)を使ってしっかりと後始末する方法が推奨されています。
これで、特に循環参照とその扱いを意識しないといけない場面でも、安定したコードが書ける道が開けます。例えば、21行目のコードで循環参照の考え方を使わない場合、どう解消するか再考する必要があります。一つの解決策としては、defer
で後始末をすることです。しかし、これは依然としてプログラマーの技量に依存します。
従って、あらかじめdefer
を使って、循環参照を解消するコードを書き、リターン直前にも確実に呼び出すようにする必要があります。将来的にコードが修正される際にも安定して後始末が行えるようにするのが理想です。 コードがうまく動作してくれるための解決方法として、スマートポインターやガベージコレクションのような仕組みを自分で実装する方法があります。例えば、Javaのように自動的にメモリ管理を行う仕組みを模倣し、リソースの解放を適切なタイミングで行うクラスを作成するのも一つのアイデアです。
このような自動解放の仕組みを導入することで、プログラマがうっかりミスをしても安全にメモリ管理が行えるようになります。たとえば、C++ではスマートポインターというクラスがあります。これを使用すると、オブジェクトがスコープを抜けた時に自動的に解放されます。Swiftでも同様のアプローチでジェネリクスなどを利用して、手動で管理しなければならないクラスに対して安全に使えるラッパークラスを作成することができます。
スマートポインターを使うことで、リソースを確実に解放することができ、循環参照問題のような課題も解決する方法が示されます。こうした解決策を逆算して導き出し、具体的な実装方法を考えることが重要です。
今日の勉強会ではこの辺りまでですが、次回以降も引き続きこの話題を詳しく見ていこうと思います。本日の内容はここまでにして、勉強会を終わりにします。お疲れ様でした。ありがとうございました。