循環参照がどういうことなのか視覚的に分かりやすく見てみる

#目次
* はじめに
* 前提確認
* サンプルコードを用意
* 強参照で繋いてみる
* 弱参照で繋いでみる
* まとめ


#はじめに
循環参照ってよく聞きますよね。関連ワードとしては強参照とか、弱参照とか、メモリが解放されないとか…。難しい概念的なことで、結局どういうことが起こってるの？何がダメなの？といまいち腹落ちしていませんでした。そこでXcodeの"Debug Memory Graph"を使ってみると、循環参照について視覚的に分かりやすかったのでまとめてみました。この記事を読むことでイメージが湧いて、他のもっと詳しい関連記事が読みやすくなるかと思います。

#前提確認
MacOS Catalina 10.15.4
Xcode 12.4
Swift version 5

最初に前提として知っておきたい知識を確認します。
とりあえず深く理解する必要はなく、こういうのがあるんだなくらいで大丈夫です。

##①強参照
これはよくみる一番シンプルな参照。

```swift
class Person{
   let name : String

   init(name : String){
       self.name = name
   }
}

var john :Person? = Person(name: "John")
var john2 = john //←実はこれが強参照
john = nil
print(john2?.name) //Optional("John")

```

これは`john2`が`john`を強参照している状態です。
`john`に`nil`を入れても`john2`から強参照されているので、`john`は消えないんですね。
このように強参照されているインスタンスはnilが入っても残り続けます。

##②弱参照
IBOutlet接続でよくみる`weak`が付いている参照。


```swift
class Person{
   let name : String

   init(name : String){
       self.name = name
   }
}

var john :Person? = Person(name: "John")
weak var john2 = john //←これが弱参照
john = nil
print(john2?.name) //nil

```
これは`john2`が`john`を弱参照している状態です。
`john`に`nil`を入れると`john2`からは弱参照しかされていないので、`john`は消えてしまいます。
なのでその後に`john2`を呼び出しても参照先が消えているので、`nil`になってしまうんですね。

##③メモリが解放されるルール
①②で見てきたように、強参照されている限り、インスタンスは消えません。なのでメモリとして残り続けます。逆に言うと、「1つでも強参照がなくなれば、そのインスタンスを消すことができ、メモリを解放できる」と言えます。このルールは覚えておいてください。

#サンプルコードを用意
前提知識を確認したところで、実際に循環参照について見ていきましょう。

まずはサンプルコードを用意します。仕様としては、
①`CustomView`を作って+ボタンと-ボタンを用意して
②`MainViewController`上の数字が入るLabelを増減させる
③`StartViewController`を最初の画面として、`MainViewController`と行き来できるようにします

`CustomView`からはpotocolを適用させた`delegate`変数を用意し、`delegate`適用先を`MainViewController`に設定します。ここの`delegate`変数の接続の仕方が今回のポイントです。

大まかにコードを見てましょう。

```swift:CustomView
import UIKit

@objc protocol ButtonActionDelegate: NSObjectProtocol { //プロトコルを用意
    func plus()
    func minus()
}

class CustomView: UIView{

    var delegate: ButtonActionDelegate? //今回はここが注目ポイント

    /*------------このからはCustomViewの初期化なので今回は関係ありません---------------*/
    override init(frame: CGRect) {
            super.init(frame: frame)
            loadNib()
        }

        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            loadNib()
        }


        func loadNib() {
            let view = Bundle.main.loadNibNamed("CustomView", owner: self, options: nil)?.first as! UIView
            view.frame = self.bounds
            self.addSubview(view)
        }
    /*------------ここまではCustomViewの初期化なので今回は関係ありません---------------*/

    @IBAction func didTapUp(_ sender: Any) {
        delegate?.plus() //delegateメソッド
    }

    @IBAction func didTapDown(_ sender: Any) {
        delegate?.minus() //delegateメソッド
    }


```

```swift:MainViewController
import UIKit

class MainViewController: UIViewController, ButtonActionDelegate { //プロトコルを適用

    @IBOutlet weak var viewForCustomView: CustomView!
    @IBOutlet weak var numberLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        print("MainViewControllerを開きました")
        viewForCustomView.delegate = self //delegate先を自分に設定
    }

    private var number = 0

    func plus() { //delegateメソッド実行
        number += 1
        numberLabel.text = "\(number)"
    }

    func minus() { //delegateメソッド実行
        number -= 1
        numberLabel.text = "\(number)"
    }
}
```

```swift:StartViewController
class StartViewController: UIViewController {

    @IBAction func exitFromMainViewController(segue: UIStoryboardSegue) {
        print("StartViewControllerに戻ってきました")
    }
}
```

#強参照で繋いでみる
では注目ポイントである以下のコードを見てましょう。

```swift:CustomView
class CustomView: UIView{

    var delegate: ButtonActionDelegate? //今回はここが注目ポイント
```
これは今強参照の状態ですね。この状態でアプリを起動し、`StartViewController`と`MainViewController`をわざと3回行き来します。その後Xcodeの"Debug Memory Graph"を見ます。

![WeChat9829f756b7b322fbbdba2cc819b56e3a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/843253/2e7b249b-2c7f-f7ff-059e-cb513673f88e.png)


![WeChat80352bfc5bfaf3676fb832944a4da1a0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/843253/dca6cfc4-8223-3ce9-df14-a842180ae628.png)

すると↑のようにアプリが起動している各ファイルやそれらの参照状態を見ることができます。(ざっくり)赤枠部分を見ると、CustomView(3)とMainViewController(3)と書かれていますね。

これがどういうことかというと、実はアプリの裏でそれぞれのファイルが3つ呼ばれていて存在し続けているんです、もう使わないのに。しかもその分メモリを占領してしまいます。今回は軽い仕様だから良いものの、これに大容量の画像とかがついてたら、すぐにメモリがいっぱいになって最悪クラッシュします。

なんでこんなことになっているのか。
それは`CustomView`が`delegate`先として`MainViewController`を強参照していて、かつ`MainViewController`が`CustomView`をまた強参照しているからなんですね。お互いがお互いを強参照しているので、`StartViewController`に戻ってもどっちも消えず、残り続けることになります。もう使わないのに。このようにお互いがお互いを強参照している状態を**循環参照**していると言います。また、こうしてファイルが残り続けメモリが解放されないことを**メモリリーク**というようです。

　
↓CustomViewからdelegate先としてMainViewControllerを強参照している
![WeChatf8df1f5ffeea4390cf621e4ea45fb708.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/843253/b68ca812-96ff-6077-6f22-b7dc608ca493.png)


↓MainViewから(間のもの含めて全て)CustomViewまで強参照している
![WeChatc09a6ac1ac356c807c6783a000c18d96.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/843253/0b983618-13aa-bb48-19ad-27d0bc4e2b9a.png)


ではどうすれば良いのか。
答えは簡単で、どちらかをweakで弱参照にすればOKです。


#弱参照で繋いでみる
では、先ほど強参照にしていた箇所を弱参照にしてみます。

```swift:CustomView
class CustomView: UIView{

    weak var delegate: ButtonActionDelegate? //今回はここが注目ポイント
```
その後同じように`StartViewController`と`MainViewController`をわざと3回行き来します。
結果は↓のように、`StartViewController`に戻ってきたことで`CustomView`も`MainViewController`も消えています。

![WeChat89707f8cc310c7d80d7cf7b7a8d0e198.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/843253/a37da24a-6f68-e15c-9f6d-4f9961abb4d4.png)

これは`CustomView`が`delegate`適用時に弱参照しているため、`StartView`に戻ってきた時に`MainViewController`がどこからも強参照されないために消え、その結果`CustomView`がどこからも強参照されなくなったので`CustomView`も消えているのです。

これで使わないファイルは消えて、無駄なメモリを使用しなくてすみますね。

#まとめ

少し長くなってしまいましたが、こうして視覚的にみるとイメージが湧きやすかったではないでしょうか。

ここで見たものは基本的な流れで、参照の仕方についてはまだまだ奥が深いですが、そうした深い知識を学ぶためのとっかかりとなれば嬉しいです。

こうして自分で色々試してみると見えてくることがありますので、是非ご自身でも色々試してみてください！