前回までに、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>" と出力
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>" と出力
paragraph
を nil
に設定できるようオプショナルな HTMLElement
として定義されています。
あいにく、上で記述した HTMLElement
クラスは、HTMLElement
インスタンスと asHTML
のデフォルト値であるクロージャの間で、強い参照の循環を生成します。
インスタンスの asHTML
プロパティは、クロージャに対する強い参照を保持しています。一方で、クロージャは本体内で(self.name
と self.text
を参照する手段として)self
を参照しているため、クロージャは self を参照しており、つまり HTMLElement
インスタンスに対する強い参照を保持しています。強い参照の循環がこの 2 つの間で生成されています。(クロージャでの値のキャプチャについての詳細な情報は、Capturing Values を確認してください。)
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.