この記事で得られる情報
- Swiftの強参照についてふわっと理解できる
- SwiftのARCについてもふわっと理解できる
- Swiftの循環参照についてもふわっと理解できる
- Swiftでメモリリークしないように実装できるようになる(可能性が上がる)
初めに
コードレビューいただいた時によく
「ここ循環参照してるから、片方弱参照にして」
なんて指摘をもらいます。
その度に「は?とりあえずweakつければいっか」ってやっていました。
しかし、これでは良くないと思い改めて循環参照について調べてきました。
強参照ってなに?
まずは強参照についてふわっと学びましょう。
強参照とは、これ使っているよっていうメモのようなものです。
例えば、部屋に箱が2つ置いてあり、箱Aには「箱B使用中」と書かれたメモが入っています。
このメモが参照と言われるもので、箱Aが箱Bを使っている(参照している)状態といえます。
通常「箱B使用中」と書かれたメモがあったら、それは強参照だと思ってもらって構いません。
「箱B使ってるから絶対に捨てるなよ」と強く言われているようなものですからね…
では実際にコードを見てみましょう。
class ClassA {
var b: ClassB?
}
class ClassB {}
var boxA = ClassA()
var boxB = ClassB()
// 箱Aが箱Bを使っている(参照している)
boxA.b = boxB
これがSwiftにおける強参照となります。
ARCってなに?
先ほど同様、箱Aと箱Bが部屋に置いてあります。
あの箱邪魔だなあ、と思っていた佐藤さんはどちらの箱も捨てようとしますが
箱Aには「箱B使用中」というメモがありました。なので、箱Bを捨てることはできません。
しかし、佐藤さんは閃きます。「箱Aを捨ててしまえば、箱B使用中というメモも捨てることができるから、その瞬間箱Bも使用中じゃなくなるよね」
ということで佐藤さんは、箱Aを捨てた後に箱Bを捨てました。
現実世界でこんなことをしたら佐藤さんは非難されますが、Swiftの世界では「不要なものを勝手に捨ててくれるいいやつ」です。この佐藤さんのような存在をSwiftではARCと言います。
※ふわっと理解なので、ARCについて詳しく知りたい方は公式ドキュメントをご覧ください
循環参照ってなに?
循環参照とは、お互いがお互いを参照しあっていることを言います。
先ほどの例で例えると
箱Aと箱Bが部屋に置いてあります。
あの箱邪魔だなあ、と思っていた佐藤さんはどちらの箱も捨てようとしますが
箱Aには「箱B使用中」というメモがあり、箱Bには「箱A使用中」というメモがあります。
この場合、どちらの箱も使用中ということになり、箱を捨てることができません。
言い換えれば、お互いの箱がお互いの箱を使用(参照)しあっている状態で、捨てることができません。
これを循環参照と言います。
では実際にコードを見てみましょう。
class ClassA {
var b: ClassB?
}
class ClassB {
var a: ClassA?
}
var boxA: ClassA? = ClassA()
var boxB: ClassB? = ClassB()
// 箱Aが箱Bを使っている
boxA?.b = boxB
// 箱Bが箱Aを使っている
boxB?.a = boxA
この、お互いがお互いを使っている(正確には参照している)状態を循環参照と言います。
循環参照しているときは箱を捨てることができないので、部屋が片付けられない状態となります。
これをメモリリークと言います。
ちょっと難しいけど大事な話
先ほどのコードで循環参照を説明しました。
では、以下のように箱を捨てるとどうなるでしょうか。
class ClassA {
var b: ClassB?
}
class ClassB {
var a: ClassA?
}
var boxA: ClassA? = ClassA()
var boxB: ClassB? = ClassB()
// 箱Aが箱Bを使っている
boxA?.b = boxB
// 箱Bが箱Aを使っている
boxB?.a = boxA
// 箱Aを捨てる
boxA = nil
// 箱Bを捨てる
boxB = nil
「箱Aも箱Bも捨てたから部屋が綺麗!」となりそうですよね。
しかし実際は違います。
「箱Aが使っている箱Bが、箱Aを使っているから綺麗にならない」というのが正解で、これこそが循環参照の正体です。
「?????」
そうなるのも仕方ないです。これは私の説明が問題で、私は
「問題を身近なものに例えて説明する」
ことを大切にしているのですが、そうすると循環参照の説明が難しすぎるので、誤って認識されることを恐れずに説明させていただきました。
メモリリークしないようにするには?
では部屋を綺麗にするにはどうすれば良いか、ですが
「箱Aが使っている箱Bが、箱Aを使っている」というのが原因なので「箱Aが使っている箱B」を捨てます。
同じように「箱Bが使っている箱A」も捨ててしまいましょう。
具体的には以下のようにします。
class ClassA {
var b: ClassB?
}
class ClassB {
var a: ClassA?
}
var boxA: ClassA? = ClassA()
var boxB: ClassB? = ClassB()
// 箱Aが箱Bを使っている
boxA?.b = boxB
// 箱Bが箱Aを使っている
boxB?.a = boxA
// 箱Aの箱Bを捨てる
boxA?.b = nil
// 箱Bの箱Aを捨てる
boxB?.a = nil
// 箱Aを捨てる
boxA = nil
// 箱Bを捨てる
boxB = nil
こうすることで循環参照することなく、部屋を綺麗にすることができます。
弱参照でもできるよ
メモリリークを回避する方法として弱参照も挙げられます。むしろこっちのやり方がメインかもしれません
ここでも2つの箱AとBを例に説明します。
箱Aには「箱B使っているけど、箱Aを使わなくなったら箱Bも捨てていいよ」と書いてあります。
箱Bには「箱A使用中」と書いてあります。
佐藤さんは「箱A捨てたら箱Bも捨てれるじゃん♪」と閃き、部屋を綺麗にすることができました。
これが弱参照です。
では、実際に弱参照を使用したコードを見てみましょう。
class ClassA {
weak var b: ClassB?
}
class ClassB {
var a: ClassA?
}
var boxA: ClassA? = ClassA()
var boxB: ClassB? = ClassB()
// 箱Aが箱Bを使っている
boxA?.b = boxB
// 箱Bが箱Aを使っている
boxB?.a = boxA
// 箱Aを捨てる
boxA = nil
// 箱Bを捨てる
boxB = nil
このようにすると、箱Aを捨てると同時に箱Bも捨てられる状態となり、循環参照を回避することができます。
以上、この記事ではSwiftの強参照、ARC、循環参照についてふわっと理解できる内容を提供しました。
また、Swiftでメモリリークを防ぐための具体的な実装方法についても学びました。