前回までに、2 つのクラスインスタンスのプロパティが互いに強い参照を持つことで、どのようにして強い参照の循環が生成されるのかを見てきました。また、強い参照の循環を切るために、弱い参照および非所有の参照を使用する方法についても見てきました。

クラスインスタンスのプロパティにクロージャを代入した場合にも強い参照の循環が起こる可能性があり、クロージャの本体がインスタンスをキャプチャします。クロージャの本体で self.someProperty のようにしてインスタンスのプロパティにアクセスするため、あるいはクロージャが self.someMethod() のようにしてインスタンスのメソッドを呼び出すため、このキャプチャが発生します。どちらのケースでも、アクセスによってクロージャが self をキャプチャし、強い参照の循環を生成します。

クラスと同様に、クロージャは参照型のため、強い参照の循環が起こります。プロパティにクロージャを代入するとき、クロージャに参照を代入しています。本質的に同じ問題で、2 つの強い参照が互いを生存させ続けます。ただし、2 つのクラスインスタンスではなく、今回はクラスインスタンスとクロージャが互いを生存させ続けます。

Swift には、この問題に対する洗練された解決策として、クロージャキャプチャリストがあります。ですが、クロージャキャプチャリストで強い参照の循環を切る方法について学習する前に、どのようにして循環が起こるかを理解しておくと役立ちます。

次の例は、self を参照するクロージャを使用したときに、どのようにして強い参照の循環が生成されるかを示しています。この例では、HTML 文書内の個々の要素をシンプルにモデル化するクラス HTMLElement を定義しています。

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

HTMLElement クラスは、見出し要素は "h1"、段落要素は "p"、改行要素は "br" のような、要素の名前を示す name プロパティを定義しています。また、HTMLElement は、その HTML 要素内に表示されるテキスト文字列を設定できる、オプショナル text プロパティを定義しています。

シンプルな 2 つのプロパティに加えて、HTMLElement クラスは遅延プロパティ asHTML を定義しています。このプロパティは、name と text を HTML 文字列に統合するクロージャを参照しています。asHTML プロパティは () -> String 型、つまり、パラメータが無く String 値を返す関数型です。

デフォルトで、asHTML プロパティに HTML タグの文字列表現を返すクロージャが代入されます。このタグは、text がある場合にはオプショナル text 値を含み、text が無い場合にはテキスト無しになります。段落要素の場合、text プロパティが "some text" か nil かによって、クロージャは "<p>some text</p>" または "<p/>" を返します。

asHTML プロパティはインスタンスメソッドのように名前が付けられ、利用されます。けれども、asHTML はインスタンスメソッドではなくクロージャプロパティであるため、特定の HTML 要素では HTML を変更したい場合、asHTML プロパティのデフォルト値を別のクロージャで置き換えることができます。

例えば、text プロパティが nil の場合に、空の HTML タグが返されることを防ぐため、なんらかのテキストをデフォルトにするクロージャを asHTML プロパティに設定することができます。

let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// "<h1>some default text</h1>" と出力
NOTE
要素を実際に HTML 出力の文字列表現にする必要があるまで不要なため、asHTML プロパティは遅延プロパティとして宣言されています。遅延プロパティは初期化が完了するまでアクセスされることがなく、self が存在することが明らかであるため、遅延プロパティである asHTML のデフォルトのクロージャ内で self を参照することができます。

HTMLElement クラスには、新しい要素を初期化するための、name 引数と(必要に応じて)text引数を受け取るイニシャライザがあります。また、クラスには HTMLElement が割り当て解除された時に表示するメッセージを出力するデイニシャライザも定義しています。

次は、HTMLElement クラスを生成して新しいインスタンスを出力する方法を示しています。

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// "<p>hello, world</p>" と出力
NOTE
以降で強い参照の循環があることを説明するため、変数 paragraph を nil に設定できるようオプショナルな HTMLElement として定義されています。

あいにく、上で記述した HTMLElement クラスは、HTMLElement インスタンスと asHTML のデフォルト値であるクロージャの間で、強い参照の循環を生成します。

image: closureReferenceCycle01_2x

インスタンスの asHTML プロパティは、クロージャに対する強い参照を保持しています。一方で、クロージャは本体内で(self.name と self.text を参照する手段として)self を参照しているため、クロージャは self を参照しており、つまり HTMLElement インスタンスに対する強い参照を保持しています。強い参照の循環がこの 2 つの間で生成されています。(クロージャでの値のキャプチャについての詳細な情報は、Capturing Values を確認してください。)

NOTE
クロージャは self を複数回参照していますが、HTMLElement インスタンスに対する強い参照を 1 つのみキャプチャします。

変数 paragraph に nil を設定して HTMLElement インスタンスに対する強い参照を切ったとしても、強い参照の循環であるため、HTMLElement インスタンスとクロージャのどちらも割り当て解除されません。

paragraph = nil

HTMLElement のデイニシャライザでのメッセージが出力されていないことに注目してください。つまり、HTMLElement インスタンスは割り当て解除されていないということです。


Portions of this page are translations based on work created and shared by Apple and used according to terms described in the Creative Commons Attribution 4.0 International License.