LoginSignup
3
2

[Swift] @escapingのないクロージャ引数でも循環参照が起こる例

Last updated at Posted at 2024-02-23

はじめに

@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などがあります。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2