Swiftの循環参照(Strong Reference Cycles)| クラスの場合

Swiftの循環参照(Strong Reference Cycles)

循環参照です。普通は「circulating references」とか「reference cycles」だと思いますが、Swiftでは強い参照であることを強調するためにstrong reference cyclesと呼ばれています。

ここでは、循環参照とはなんぞや?から始めて、特にクラス間での循環参照を避けるための具体的な方法、弱参照と非所有参照について詳しく説明していきます。さらに、非所有参照とオプショナル型を組み合わせるケースについても説明します。

循環参照(Strong Reference Cycles)とは何か?

Swiftの自動参照カウントのページでは、クラスAccountを作って、強参照(または強参照カウント)が残っている限りメモリが解放されないことを確認しました。

一方で、強参照がゼロにならないようにクラスインスタンスを作ることが、実は可能です。どうすればそんなことになるでしょうか?一番単純に考えると、2つのクラスインスタンスがお互いの強参照を持っているケースです。

下の図は、後で示す循環参照の具体例を単純化した図ですが、

[図解]循環参照の概念図

こうすると、例えば「強参照1」を解放しても、「強参照2」でApplicationインスタンスからAccountインスタンスに対する強い参照が残っているので、結局メモリが解放できません。逆のケースも全く同様です。このようなケースを一般的に循環参照と呼んでいます。

Swift公式マニュアルでは「strong reference cycles」という言葉を使っていて、ここではそれを「循環参照」としています。厳密には「強い参照の循環」とか「強参照の循環」等の訳が正しいかもしれませんが、循環参照の方が馴染みがあるので、ここでは循環参照を使います。

循環参照を解決する方法はこの後詳しく説明しますが、その前に循環参照がどのように生じるのかを、具体例を用いてもっと詳しく見ていきます。

クラス間の循環参照 | AccountApplicationクラス

Swiftの自動参照カウントのページで使ったAccountクラスを改造して、クラス間の循環参照を具体的に説明します。

// 循環参照の具体例
class Account {
    let name: String
    var app: Application?

    init(name: String) {
        self.name = name
        print("\(name)を初期化")
    }
    deinit {
        print("\(name)を破棄")
    }
}

class Application {
    let name: String
    var account: Account?

    init(name: String) {
        self.name = name
        print("アプリ\(name)を初期化")
    }
    deinit {
        print("アプリ\(self.name)を破棄")
    }
}

この例では2つのクラスAccountApplicationを定義しています。何かしらのアカウントとアプリケーションが、お互いの情報を参照するというイメージです。

オプショナル型でお互いのプロパティを保有

AccountクラスはString型の格納プロパティnameと、オプショナル型(optionals)Accountクラスのプロパティappを持っています(初期値はnil)。

class Account {
    let name: String
    var app: Application? // AccountがApplicationを持っている
    ....
}

同様に、Applicationクラスは、アプリの名前nameString型で、さらにアプリ所有者の情報をAccount?(オプショナル型のAccountクラス)プロパティとして持っています。

class Application {
    let name: String
    var account: Account? // ApplicationがAccountを持っている
    ....
}

AccountApplicationクラスインスタンスを生成

循環参照をテストするために、それぞれのクラスインスタンスを作ってみます。

var tarou: Account?
var terminal: Application?

print("インスタンス生成")
tarou = Account(name: "太郎")
terminal = Application(name: "ターミナル")

//出力
//インスタンス生成
//太郎を初期化
//アプリターミナルを初期化

それぞれのクラス変数をオプショナル型にしたのは、最後に明示的にnilにして終了子(deinitializer)が呼び出せるかを見たいからです。クラスインスタンスはちゃんと作られているのが分かります。アカウントの名前は適当で、アプリは「ターミナル」を想定しました(名前だけなので、別に何でも構いません)。

公式マニュアルの図が大変分かりやすいので、同じものを作ってみると、現在の状態は

循環参照の具体例、それぞれの変数を初期化

こんな感じです。それぞれの変数(tarouterminal)がクラスインスタンスを強く参照しています。ただし、この時点では循環参照は発生していません。

循環参照の生成

では、いよいよ(意図的に)循環参照を作ってみます。

print("循環参照")
tarou!.app = terminal
terminal!.account = tarou

上記のソースコードでは、Accountが持つappプロパティにterminalを代入し、同様にApplicationが持つaccountプロパティにtarouを代入しています。この時、tarouterminal変数はオプショナル型ですから、forced unwrappingしてからプロパティにアクセスしています。

この状態を図示化すると、

循環参照の具体例、内部変数に参照を代入

という感じになります。図にすると、これら2つのクラスがお互いを強く参照していることが良く分かると思います。AccountクラスはappプロパティからApplicationインスタンスを強く参照しており、またApplicationクラスはaccountプロパティを通じてAccountインスタンスを強く参照しています。

循環参照は消えない

ここで、大元の変数にnilを代入するとどうなるでしょうか?

print("インスタンスを破棄できるか?")
tarou = nil
terminal = nil
// 何も表示されない

試してみると分かると思いますが、何も起こりません(終了子が呼び出されない、という意味です)。つまり、参照カウントがゼロにならず、ARCがインスタンスのメモリを解放できないということになります。

この状態を図示化すると、以下のようになります

循環参照の具体例、外部変数を破棄すると循環参照が残る

変数tarouterminalからの強い参照は当然無くなりましたが、インスタンス内部での循環参照が残ります。この相互の参照はどうやっても解除することができませんので、メモリリークの原因になってしまいます。

現実問題似たような実装をするケースがあるかもしれません。「じゃあどうすれば循環参照を避けられるんだ!」という疑問が湧きますが、それを解決する方法を次のセクションで説明します。

クラス間の循環参照を解決する方法

Swiftでは、循環参照(strong reference cycles)を避ける方法が2つあります。1つは弱参照(Weak References)で、もう1つは非所有参照(Unowned References)です。

弱参照または非所有参照を使うことによって、循環参照を発生させずに、インスタンスがお互いを参照することが可能になります。以下、具体例を交えつつ、詳しく説明していきます。

弱参照(Weak References)

弱参照とは何か?

弱参照(Weak References)は、一言で言えば

自動参照カウントによってカウントされない参照

です。

これまで見てきたように、循環参照ではどうやっても参照カウントをゼロにすることが出来ません。参照カウントをゼロに出来ないので、結局メモリを解放できません。「じゃあ、そもそも参照カウントにカウントされない参照を作ればいいんじゃね?」ということで導入されたのが弱参照です。弱参照であることを示すには、プロパティや変数宣言の前にweakキーワードを付けます。

弱参照は参照先の値がnilでも良い

弱参照は、その参照先の値がnilに変わる可能性がある場合に使います。もし、参照先が常に何かしらの値を持っている(絶対にnilにならない)場合は、後述する非所有参照(unowned references)を使います。

上記の例ですと、Accountクラスでは、保有するApplicationクラスのプロパティappnilに成り得ます。また、逆も同様(Applicationが持つAccountクラスのプロパティaccountnilになり得る)です。したがって、この場合、循環参照を解決する方法としては、弱参照を使うのが正しいです。

弱参照はオプショナル型、また変数でなければならない

弱参照は参照カウントされないので、弱参照がインスタンスを参照しているにも関わらず、そのインスタンスのメモリが解放される可能性があります。したがって、ARCは、弱参照が参照しているインスタンスのメモリが解放された場合、自動的に弱参照にnilをセットします。また、「弱参照はnilになる可能性がある」ということから、弱参照は必ずオプショナル型になることが分かります。また、実行時にnilに変わる可能性があるオプショナル型ですから、弱参照は必ず変数として定義します(途中で値が変わる可能性があるため)。定数としては定義することが出来ません。

“And, because weak references need to allow their value to be changed to nil at runtime, they are always declared as variables, rather than constants, of an optional type.”

抜粋:: Apple Inc. “The Swift Programming Language (Swift 3)”。 iBooks https://itun.es/jp/jEUH0.l

具体例で見る弱参照

最初に使った例を流用していますが、AccountクラスのApplicationプロパティappが弱参照になっています。どちらを弱参照にしても良いと思いますが、一応「アカウントがあっても、(ある特定の)アプリケーションを持つ必要はない」ということを想定して、弱参照をつけています。

// 弱参照の具体例
class Account {
    let name: String
    weak var app: Application?

    init(name: String) {
        self.name = name
        print("\(name)を初期化")
    }
    deinit {
        print("\(name)を破棄")
    }
}

class Application {
    let name: String
    var account: Account?

    init(name: String) {
        self.name = name
        print("アプリ\(name)を初期化")
    }
    deinit {
        print("アプリ\(self.name)を破棄")
    }
}

var tarou: Account?
var terminal: Application?

print("インスタンス生成")
tarou = Account(name: "太郎")
terminal = Application(name: "ターミナル")

print("相互参照")
tarou!.app = terminal
terminal!.account = tarou

ここまでは、最初に示した例と同じです。

公式マニュアルにあるように、この状態を図示すると

[図解]弱参照の具体例、相互参照

という感じになります。Applicationインスタンスはaccountプロパティを通じてAccountインスタンスを強参照していますが、AccountインスタンスはappプロパティからApplicationインスタンスを弱参照しています。

今、terminal変数にnilを代入すると、

terminal = nil
//"アプリターミナルを破棄"と表示

という具合に、終了子が呼び出されていることが分かります。nilを代入したということは、terminal変数からApplicationインスタンスに対する強参照を破棄したことになります。

この時点で、Applicationインスタンスに対する強参照が無くなりました。弱参照は、参照先のインスタンスが破棄された場合nilが代入されますから、Accountインスタンスが持っているappプロパティにnilがセットされます。

本当にそのように動作しているか確認してみます。

// appプロパティの確認
if let app = tarou!.app {
    print("名前: \(app.name)")
}
//"名前: ターミナル"と表示

// terminal変数にnilを代入
terminal = nil

// appプロパティの確認(appプロパティはnilになっているはず)
if let app = tarou!.app {
    print("名前: \(app.name)")
} else {
    print("appプロパティはnilです")
}
//"appプロパティはnilです"と表示。

Optional bindingを使って確認すると、確かにterminal変数にnilを代入した後は、tarouが持っている弱参照のappプロパティがnilになっていることが分かります。

[図解]弱参照の具体例、terminalを破棄

この時点で残っている強参照は、tarou変数からAccountインスタンスに対する強参照のみです。したがって、tarounilを代入すると、

tarou = nil
//"太郎を破棄"と表示

終了子がちゃんと呼びだされ、この具体例で用意した全てのインスタンスが破棄されたことになります。

非所有参照(Unowned References)

非所有参照(Unowned References)とは何か?

非所有参照(Unowned References)は基本的には弱参照と一緒です。違いは、

非所有参照は常に値を持つ

ということです。つまり、非所有参照は非オプショナル型(nonoptional type、つまり普通の型)で定義されなければいけません。非所有参照であることを示すには、プロパティ(または変数)宣言の際にunownedキーワードを付けます。

非所有参照は非オプショナル型でなければならない

先程言いましたが、

非所有参照は非オプショナル型(普通の型)

になります。

非所有参照はオプショナル型ではないので、使用する際にはunwrapする必要はありません(常に直接アクセスすることが出来ます)。ただし、非所有参照が参照しているインスタンスが破棄された場合は、値としてnilを代入することが出来ませんので、注意が必要です。

非所有参照の具体例

先程弱参照で使った例を改良して、非所有参照の説明をします。

// 非所有参照の具体例
class Account {
    let name: String
    var app: Application?

    init(name: String) {
        self.name = name
        print("\(name)を初期化")
    }
    deinit {
        print("\(name)を破棄")
    }
}

class Application {
    let name: String
    unowned var account: Account

    init(name: String, account: Account) {
        self.name = name
        self.account = account
        print("アプリ\(name)を初期化")
    }
    deinit {
        print("アプリ\(self.name)を破棄")
    }
}

var tarou: Account?

print("インスタンス生成")
tarou = Account(name: "太郎")
// Accountインスタンス経由で、Applicationクラスインスタンスを作る
tarou!.app = Application(name: "ターミナル", account: tarou!)

print("tarouにnilを代入")
tarou = nil

非所有参照における2つのクラス(AccountApplication)の関係は、弱参照の場合のそれとはちょっと違います。アカウント(Accountクラス)はアプリ(Applicationクラス)を持つかもしれないし、持たないかもしれません。別に必要のないアプリなら、わざわざダウンロードする必要がないからです(という想定です)。しかし、アプリの方は必ずアカウントに紐付いています。

弱参照はAccountの持つApplicationプロパティに付けていましたが、非所有参照はApplicationの持つAccountプロパティに付けました。論理的に説明しやすいように付けただけなので、動作確認するだけならどちらでも良いと思います。

これを表現するため、Accountクラスの持つApplicationプロパティはオプショナル型になっていて、

class Account {
    var app: Application?
    ....
}

一方で、ApplicationクラスのもつAccountクラスは、非オプショナル型(普通の型)になっています。

class Application {
    unowned var account: Account
    ....
}

また、非オプショナル型ですから、初期化子(initializer)内部でちゃんと初期化しないといけません。

init(name: String, account: Account) {
    self.name = name
    self.account = account
    print("アプリ\(name)を初期化")
}
弱参照の場合はnilで初期化されますので、特に初期化する必要はありません。

Accountインスタンスは、これまでと同様にオプショナル型で作ります。

var tarou: Account?
tarou = Account(name: "太郎")

弱参照を使った例では、Applicationインスタンスも独立して作りましたが、今回の例ではAccountインスタンスが所有するApplicationプロパティに新しいインスタンスを代入します。

tarou!.app = Application(name: "ターミナル", account: tarou!)

この状態を図示すると、

[図解]非所有参照の具体例、

という具合になります。当然、変数tarouAccountインスタンスを強く参照しています。今、Accountインスタンスは、プロパティappを通してApplicationインスタンスを強く参照しています。それに対して、Applicationインスタンスは、accountプロパティでAccountインスタンスを参照していますが、これが非所有参照になっています。

ここで、tarou変数が保持するAccountインスタンスを破棄すると、どうなるでしょうか?

print("tarouにnilを代入")
tarou = nil
//tarouにnilを代入
//太郎を破棄
//アプリターミナルを破棄

tarou変数からAccountインスタンスへの強参照を解くと、Accountインスタンスに対する強参照が無くなります。Accountインスタンスに対する強参照が全て無くなったので、この時点でARCがAccountインスタンスのメモリを解放します。

Accountインスタンスが破棄されると、Applicationインスタンスに対する強参照(図の右向きの黒矢印)が無くなりますので、Applicationインスタンスも破棄されます。つまり、

Accountインスタンスが破棄された後に、Applicationインスタンスが破棄

という順序で実行されることが分かります。実際に、tarou変数にnilを代入した後に、「太郎を破棄」(Accountインスタンスの破棄)と表示された後に、「アプリターミナルを破棄」(Applicationインスタンスの破棄)が表示されているのが分かります。

非所有参照とImplicitly Unwrapped Optional

プロパティが両方nilにならない場合

ここまでで、弱参照と非所有参照を使って、循環参照を回避する2つの具体的なケースを見てきました。箇条書きにすると、

  • 2つのプロパティが両方nilになる場合 → 弱参照
  • 1つのプロパティがnil、もう1つはnilにならない場合 → 非所有参照

という感じです。これを見るとすぐに想像できると思いますが、

2つのプロパティが両方nilにならない場合

という状況もあるはずです。今回はこのケースを説明します。

結論から言うと、

2つのプロパティが両方nilにならない場合は、非所有参照とimplicitly unwrapped optionalsを組み合わせて使う

と、循環参照をうまく解決できます。

非所有参照とImplicitly Unwrapped Optionalを使う場合の具体例

早速具体例で見てみます。

// 非所有参照とimplicitly unwrapped optionals
class Company {
    let name: String
    var president: Worker!
    init(name: String, workerName: String) {
        self.name = name
        self.president = Worker(name: workerName, company: self)
    }
}

class Worker {
    let name: String
    unowned let company: Company
    init(name: String, company: Company) {
        self.name = name
        self.company = company
    }
}

構造としては公式マニュアルにある例と全く一緒です。ここでは、会社Companyと社員Workerの関係を使っています。ここで想定している関係は、会社には必ず社長(presidentプロパティ)がいて、社員は必ず会社(companyプロパティ)に属している、ということです。

Workerクラスの初期化子は、一つ前の非所有参照の場合と全く一緒です。非所有参照は普通の型なので、初期化子できっちり初期化してやる必要があります。

// プロパティを初期化
init(name: String, company: Company) {
    self.name = name
    self.company = company
}

Companyクラスの初期化子では、Workerプロパティpresidentを初期化するのに、自分自身であるselfをパラメータとして渡さないといけません。

// selfを渡す必要がある
init(name: String, workerName: String) {
    self.name = name
    self.president = Worker(name: workerName, company: self) // <-- selfを渡す
}

2段階初期化における4つの安全確認、で詳しく説明しましたが、

selfプロパティは、1段階目の初期化が完了しないと使えない

です。1段階目の初期化は、自分が持つ格納プロパティ(及びスーパークラスの格納プロパティ等々)を初期化するプロセスです。したがって、selfを使う前に、自分自身がもっている格納プロパティを全て初期化しないといけません。

改めて上記の初期化子を見てみると、

// 先に格納プロパティを初期化
self.name = name
// presidentはimplicitly unwrapped optionalsなので、nilで初期化されている
// --> この時点で1段階目の初期化が完了
self.president = Worker(name: workerName, company: self) // <-- selfを渡す

という具合で、selfを渡す前にnameプロパティを初期化しています(この順序が逆だとコンパイルエラーになります)。

presidentプロパティはオプショナル型(implicitly unwrapped optionals)なので、宣言時にnilで初期化されています。したがって、nameプロパティが初期化された時点でCompanyクラスが持つ全ての格納プロパティが初期化されたことになります。Companyクラスは基底クラス(base class)ですから、初期化の委譲(initializer delegation)も必要ありませんので、ここで1段階目の初期化が完了して、selfプロパティが使えるようになるわけです。

Companyクラスインスタンスを作ると、presidentプロパティに直接アクセス出来ます。

var company = Company(name: "会社", workerName: "太郎")
print("社長の名前: \(company.president.name)")
//"社長の名前: 太郎"と表示

Implicitly unwrapped optionalsを使っているので、unwrapせずに普通のプロパティのようにアクセスすることが出来ます。循環参照していないかの確認はしていませんが、前の2つのケースと同様に、終了子を作ってインスタンスの生成と破棄を試してみると、ちゃんと循環参照が回避されているのが分かると思います。

実際問題、implicitly unwrapped optionalsにする利点は、インスタンスコール時に毎回unwrapしなくて良いということではないかと思います。ですので、普通にオプショナル型を使っても、上記のケースでは問題なくコンパイル出来、実際に使えます。

まとめ

  • 循環参照(strong reference cycles)とは、参照カウントがゼロにならないこと
  • 循環参照が起こるとメモリを解放出来ないので、メモリリークの原因になる
  • 弱参照(weak references)は、相互参照元が両方nilになる場合に有効
  • 非所有参照(unowned references)は、参照元がnilになるモノとそうでないプロパティが混在している場合に有効
  • 参照元が両方nilにならない場合、非所有参照とオプショナル型(implicitly unwrapped optionalsがベター)を組み合わせて使う