関数やクロージャは,実行時にはただのオブジェクトインスタンス扱いなので,ARCで正しく解放されるように循環参照に気を付けよう,というのは理解できる.
では,どういった記述をした場合に循環参照になるのだろうか? キャプチャリスト(Capture List)の使い処がいまいち分からなかったので調べてみた.
クラス内部にクロージャ(関数)として記述するのは以下の三種でしょうか.
- クラス内で定義した関数(メソッド)
- クラスのプロパティとしてのクロージャ
- メソッド内で定義されたクロージャ
クラス内で定義した関数(メソッド)
class A {
var v: Int = 0
func f() {
[unowned, self] in // <== 文法違反.inもキャプチャリストも書けない.
return ++(self.v)
}
}
メソッド(関数)定義は,文法的にはクロージャとは異なるためin
はおろかキャプチャリストさえ書くことはできない.関数内部でselfを参照しているのに.これは,つまるところ言語仕様的には循環参照と見なさないということであろう.まあ,こんなのでいちいち陽に循環参照になってたら面倒でやっていられない.
クラスのプロパティとしてのクロージャについて
class B {
var v: Int = 0
var f: () -> Int = { ++(self.v) } // <== 識別子selfを解決できないとエラー
}
プロパティに代入するクロージャ内で'self'を参照するとエラーになる.しかし,
class C {
var v: Int = 0
lazy var f: () -> Int = { ++(self.v) } // lazyを付けるとO.K.
}
のようにlazy
を付けるとselfを参照できるようになる!?
なぜこれでO.Kとなるのか理由を考えてみる.おそらくインスタンス初期化手順のためであろう.インスタンス化するとき,プロパティで参照してるオブジェクトが先んじて初期化される.この時点ではBのインスタンスはまだ初期化されていないので,先に初期化されるクロージャからself
を参照できないのはうなずける.または,こうも考えられる.クロージャの静的スコープにself
という識別子がそもそもない(コード上にそんな識別子はない).
一方,lazy
を付けると,その名の通り,後から必要に応じて初期化される.ゆえに,それが初期化される時点ではCのインスタンスはすでに存在し'self'を参照できる.処理系内部ではクロージャのスコープに'self'を追加しているのだろうと想像.ある意味,動的スコープっぽいのかな.
そして,Cのインスタンスとクロージャの初期化が独立しているがために,陽に循環参照が生じることになる.こういう場合にキャプチャリストが必要となる.
class D {
var v: Int = 0
lazy var f: () -> Int = {
[unowned self] in // <== これを付けないと循環参照
++(self.v)
}
}
メソッド内で定義されたクロージャについて
class E {
var v: Int = 0
var g: (() -> Int)!
func f() {
g = {
++(self.v)
}
}
}
この例は素直にコンパイルも通るし実行もできる.(2)の場合と同様で,このクロージャはクラスEのインスタンスとは別個に後で初期化される(のであろう).そしてこの場合も循環参照が生じるから下記のようにキャプチャリストは必須である.
class F {
var v: Int = 0
var g: (() -> Int)!
func f() {
g = {
[unowned self] in // 必須!
++(self.v)
}
}
}
以上です.
要は,クラスのインスタンスと独立で(後で必要になって)初期化されるようなクロージャを定義するときに,そのクロージャ内でself
(またはその他の参照型変数)を参照していて,かつ,クラスでもそのクロージャを参照しているのであれば,循環参照を防ぐためにキャプチャリストが必須となるわけでした.
上記の各クラスにdeinit {println("dealloc'ed")}
などを追加してメモリリークが起こるかどうかを確かめてみるとはっきり分かると思います.