楽水

人々の創造が自由に表現できる舞台づくり

Swift

Swiftクロージャによる強い参照の循環参照とその解決

投稿日:

Swiftのメモリ管理
では、
2 つのクラスインスタンスのプロパティが互いに強い参照を持つことで、どのようにして強い参照の循環参照が生成されるのかを見てきました。
また、強い参照の循環参照を切るために、弱い参照および非所有参照を使用する方法についても見てきました。
しかし、2つのクラスインスタンスのプロパティだけでなく、クラスインスタンスのプロパティにクロージャを代入した場合にも強い参照の循環参照が起こる可能性があります。
そこで、今回は、以下の観点でクロージャによる強い参照の循環参照とその解決について解説します。

  • Swiftのクロージャによる強い参照の循環参照はなぜ発生するのか
  • どのようにしてSwiftのクロージャによる強い参照の循環参照を解決するのか

参考本
[改訂新版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plus

Swiftのクロージャによる強い参照の循環参照はなぜ発生するのか

クラスインスタンスのプロパティにクロージャを代入した場合、クロージャの本体がインスタンスをキャプチャすることになります。
例えば、クロージャの本体で self.someProperty のようにしてインスタンスのプロパティにアクセスする場合や、
クロージャが self.someMethod() のようにしてインスタンスのメソッドを呼び出す場合、
クロージャが self をキャプチャします。
クラスと同様に、クロージャは参照型のため、クラスインスタンスが持つクロージャが本体のプロパティをキャプチャする場合強い参照の循環参照が起こります。
なお、クロージャについては
Swiftのクロージャ
を参照してください。
さて、次の例は、指定したHTML要素と文章を返すクラスを定義したものです。

class HTMLElement {
 let name: String
 let text: String?
 lazy var asHTML: () -> String = {
   if let text = self.text {
    return “<\(self.name)>\(text)
   } else {
    return “<\(self.name) />”
   }
  }
 init(name: String, text: String? = nil) {
  self.name = name
 self.text = text
  }
 deinit {
  print(“\(name) is being deinitialized”)
 }
}

このクラスは、初めて利用されるまで初期値が算出されない遅延(lazy)プロパティasHTMLを定義しています。
また、このプロパティは、name と text を HTML 文字列に統合するクロージャを参照しています。
asHTML プロパティは () -> String 型、つまり、パラメータが無く String 値を返す関数型です。
関数型については
Swiftの関数型
を参照してください。
さて、このクラスを、次のように利用してみましょう。

var paragraph: HTMLElement? = HTMLElement(name: “p”, text: “hello, world”)
print(paragraph!.asHTML())
//”<p>hello, world</p>” と出力

ちなみに、ここでは、強い参照の循環があることを説明するため、変数paragraphをnilに設定できるようオプショナルな HTMLElement として定義されています。
オプショナルについては、
Swiftのオプショナル
を参照してください。
上記の例の場合、以下のようにクラスインスタンスとクロージャの間に強い参照の循環参照が発生します。


なので、次のように参照を消してもデイニシャライザ(deinit)で定義された文言は出力されません。

paragraph = nil

どのようにしてSwiftのクロージャによる強い参照の循環参照を解決するのか

それでは、どのようにして上記循環参照を解決するのでしょうか。
それは、クロージャの定義の一部としてキャプチャリストを定義することによって解決します。
キャプチャリストには、クロージャの本体内で 1 つ以上の参照型をキャプチャするときに利用するルールを定義します。
例えば、以下の例のように、キャプチャリストには、(self のような)クラスインスタンスや、(delegate = self.delegate! のように)ある値で初期化される変数に対する参照の weak や unowned キーワードのペアを定義して、クロージャのパラメータリストと戻り値の型の前に置きます。

lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate] (index: Int, stringToProcess: String) -> String in
// クロージャ本体
}

なお、キャプチャするクロージャとインスタンスが常に互いを参照し、常に同時に割り当て解除されるような場合には、非所有の参照としてクロージャ内にキャプチャを定義します。
一方で、キャプチャされた参照がその後 nil になるような場合には、弱い参照としてキャプチャを定義します。

それでは、このキャプチャリストを使って上のクラス定義の例を書き換えてみましょう。

class HTMLElement {

let name: String
let text: String?

lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return “<\(self.name)>\(text)
} else {
return “<\(self.name) />”
}
}

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

deinit {
print(“\(name) is being deinitialized”)
}

}

このクラス定義では、非所有参照である[unowned self] が定義されています。
この場合、次の例のように

var paragraph: HTMLElement? = HTMLElement(name: “p”, text: “hello, world”)
print(paragraph!.asHTML())

クラスのインスタンスを利用すると、以下のように強い参照が非所有参照になります。

なので、次のように参照を消するとデイニシャライザ(deinit)で定義された文言が出力されます。

paragraph = nil
// “p is being deinitialized” と出力

以上、今回は、クロージャによる強い参照の循環参照とその解決について解説しました。

-Swift
-, , , , ,

執筆者: