はじめに
@escaping
なクロージャで self
を強参照すると循環参照の危険があるのはご存知の方も多いでしょう。
以下は @escaping
なクロージャで循環参照が起こる例です。
Main().main()
class Main {
let test = Test()
var value = 0
deinit { print("deinit") } // 呼ばれない
func main() {
test.set {
self.value += 1
}
}
}
class Test {
var closure: (() -> Void)?
func set(closure: @escaping () -> Void) {
self.closure = closure
}
}
この例では Test.set(closure:)
が受け取ったクロージャをプロパティに保存しているため @escaping
がついています。なので循環参照を回避するために [weak self]
や [unowned self]
をつけて self
を弱参照する必要があります。
func main() {
- test.set {
- self.value += 1
+ test.set { [weak self] in
+ self?.value += 1
}
}
では、@escaping
がついてないクロージャ引数であれば循環参照の危険はないのでしょうか?
Optionalなクロージャ引数
Test.set(closure:)
の引数をOptionalに変えてみましょう。
Main().main()
class Main {
let test = Test()
var value = 0
deinit { print("deinit") } // 呼ばれる?
func main() {
test.set {
self.value += 1
}
}
}
class Test {
var closure: (() -> Void)?
- func set(closure: @escaping () -> Void) {
+ func set(closure: (() -> Void)?) {
self.closure = closure
}
}
すると @escaping
が消えてしまいました。この場合 @escaping
をつけるとコンパイルエラーになります。
@escaping
がないということは、このコードは循環参照しないのでしょうか?
実行してみれば分かりますが、このコードはしっかり循環参照します。
なぜ @escaping
がつかないのか
@escaping
がつかないのは引数が クロージャ型 ではなく Optional型 だからです。
こう書くと分かりやすいかも知れません。
func set(closure: Optional<() -> Void>)
敢えてここに @escaping
を書こうとすると以下のコンパイルエラーが出ます。
Closure is already escaping in optional type argument
訳:クロージャはOptional型の引数で既にエスケープされています
「既にエスケープされている」とはどういうことでしょうか?
Optionalの内部を理解するために、Optional<() -> Void>
型を MyOptional
という名前で独自に定義してみましょう。
class Test {
var closure: MyOptional = nil
func set(closure: MyOptional) {
self.closure = closure
}
}
enum MyOptional: ExpressibleByNilLiteral {
case some(() -> Void)
case none
init(_ some: @escaping () -> Void) {
self = .some(some)
}
init(nilLiteral: ()) {
self = .none
}
}
MyOptional.init(_:)
の引数が @escaping
になっていますね。
ここで、Test.set(closure:)
を呼ぶ側のコードを見てみましょう。
func main() {
test.set(closure: MyOptional {
self.value += 1
})
}
ここまで来れば分かるはず、このクロージャがエスケープされるのは MyOptional
での話であって、Test
には無関係だということです。
なので Test.set(closure:)
の方には @escaping
はつきません。
まとめ
Optionalなクロージャは @escaping
がなくてもエスケープされる可能性があるため、強参照しないよう注意しましょう。
Optionalなクロージャを引数に持つ身近なものとして、UIAlertActionなどがあります。