[iOS] swiftでクロージャーに@escapingをつける場合を調べてみた

2017/10/18

こんにちは。きんくまです。

自分でクロージャーを使ったメソッドを作りたかったときに、@escapingのことをよくわからなかったので調べたメモです。

参考サイト
>> The Swift Programming Language (Swift 4): Closures
>> What Do @escaping and @noescape Mean In Swift 3

@escapingしない場合

まず、@escapingしないクロージャーの場合を作ってみました。

class MathUtil{
    var num1:Int
    var num2:Int
    
    init(num1:Int, num2:Int){
        self.num1 = num1
        self.num2 = num2
    }
    
    //クロージャーの返り値を10倍する
    func tenTimes(equation:((Int, Int)->Int)) -> Int {
        return equation(num1, num2) * 10
    }
}

class ViewController: UIViewController {
    
    var num3:Int = 2
    var util:MathUtil? = MathUtil(num1: 5, num2: 3)

    override func viewDidLoad() {
        super.viewDidLoad()
        
        test_tenTimes1()
        test_tenTimes2()
        test_tenTimes3()
    }

    func test_tenTimes1(){
        guard let util = self.util else { return }
        let result = util.tenTimes { (num1:Int, num2:Int) -> Int in
            return num1 + num2
        }
        print("test_tenTimes1 result = \(result)") //result = 80
    }
    
    func test_tenTimes2(){
        guard let util = self.util else { return }
        let result = util.tenTimes { (num1:Int, num2:Int) -> Int in
            return num1 - num2
        }
        print("test_tenTimes2 result = \(result)") //result = 20
    }
    
    func test_tenTimes3(){
        guard let util = self.util else { return }
        //selfをつけていないことに注目
        let result = util.tenTimes { (num1:Int, num2:Int) -> Int in
            return num1 + num2 + num3
        }
        print("test_tenTimes3 result = \(result)") //result = 100
    }
}

MathUtilというクラスの中で、クロージャーを引数にとるメソッドを作りました。
tenTimes()は、引数のクロージャーの返り値を10倍するメソッドです。

MathUtilを使うのは、実際の場面でありそうなViewControllerが使うことにしました。

@escapingをつけるときと、つけないときの場合分け

@escapingをつけなくてもいいのは以下の場合です。

A-1. クロージャーがプロパティとして保存されない(強参照されない)
かつ
A-2. クロージャーがメソッド内ですぐに実行される(非同期でない)

逆に、@escapingをつけるのはその逆で以下になります。

B-1. クロージャーがプロパティとして保存される(強参照される)
または
B-2. クロージャーがメソッド内ですぐに実行されない(非同期である)

B-1かB-2のどちらかの場合は@escapingする必要があります。
上のコード例では、どちらの場合でもないため、@escapingする必要がありませんでした。

B-1. クロージャーがプロパティとして保存される(強参照される)場合

class MathUtil{
    ..同じところは省略
    var storedEquation:((Int, Int)->Int)?

    //クロージャーを内部で保持する
    func tenTimesWithStore(equation:@escaping ((Int, Int)->Int)) -> Int {
        self.storedEquation = equation //ここで保持
        return equation(num1, num2) * 10
    }
}

class ViewController: UIViewController {
    ..同じところは省略
    override func viewDidLoad() {
        super.viewDidLoad()
        test_tenTimesWithStore()
        test_tenTimesWithStore2()
    }

    func test_tenTimesWithStore(){
        guard let util = self.util else { return }
        let result = util.tenTimesWithStore { (num1:Int, num2:Int) -> Int in
            return num1 + num2
        }
        print("test_tenTimesWithStore result = \(result)") //result = 80
    }
    
    func test_tenTimesWithStore2(){
        guard let util = self.util else { return }
        
        //内部でselfを使う場合は、weakかunownedで参照する
        let result = util.tenTimesWithStore {
            [unowned self](num1:Int, num2:Int) -> Int in
            
            self.num3 = 1
            return num1 + num2 + self.num3
        }
        print("test_tenTimesWithStore2 result = \(result)") //result = 90
    }
}

クロージャーがメソッド内などからプロパティとして保持される場合は、@escapingする必要があります。(今回はMathUtil.tenTimesWithStore)
また、クロージャー内で呼び出し元のプロパティやメソッドを呼びたい場合は、weakかunownedをつけた方がよいです。
つけないと循環参照になってしまい、メモリーリークしてしまいます。

>> Swift 3 の @escaping とは何か

weakとunownedの使い分けなのですが、こちらが参考になりました。(これを調べるまで、私がいつもunownedしか使ってなかったというのは秘密ですw)
今回ViewControllerは破棄されない場面を想定して、test_tenTimesWithStore2の中ではunownedを使っています。

>> Swiftのクロージャにおける循環参照問題でunownedとweakの使い分けがわからない

また、気がついた人がいるかもしれませんが、@escapingしない場合は、クロージャー内でselfをつける必要がないみたいです。(Appleの公式に書いてあった。今回でいうと一番はじめのコードです)

B-2. クロージャーがメソッド内ですぐに実行されない(非同期である)場合

class MathUtil{
    //非同期後に実行する場合(今回はタイマー)
    func tenTimesWithAsync(callback:@escaping ((Int)->Void)) -> Void {
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) { [weak self] in
            //ここはtenTimesWithAsyncメソッドが終わったあとに非同期で実行される
            guard let myself = self else { return }
            callback((myself.num1 + myself.num2) * 10)
        }
    }
}

class ViewController: UIViewController {
    ..同じところは省略
    override func viewDidLoad() {
        super.viewDidLoad()
        test_tenTimesWithAsync()
    }

    func test_tenTimesWithAsync(){
        guard let util = self.util else { return }
        
        //内部でselfを使う場合は、weakかunownedで参照する
        util.tenTimesWithAsync { [unowned self](result:Int) in
            print("test_tenTimesWithAsync result \(result)") //result = 80
            
            self.num3 = 4
            let result2 = self.num3 + result
            print("test_tenTimesWithAsync result2 \(result2)") //result = 84
            
            self.util = nil
        }
    }
}

この非同期実行のときの方が、実際の場面では多いのではないでしょうか。
今回は1秒後にクロージャーが実行されて、計算結果が返ってくるというサンプルにしてみました。

補足 swift2までの@escaping

swift3から@noescapeがデフォルトの挙動になったみたいです。だから、escapeするときは、@escapingの記述が必要になったということ。
swift2までは逆で、escapeがデフォルトだったので、noescapeするときだけ@noescapeを記述する必要があったみたい。

コード全部

import UIKit

class MathUtil{
    var num1:Int
    var num2:Int
    
    var storedEquation:((Int, Int)->Int)?
    
    init(num1:Int, num2:Int){
        self.num1 = num1
        self.num2 = num2
    }
    
    deinit{
        print("deinit MathUtil")
    }
    
    //クロージャーの返り値を10倍する
    func tenTimes(equation:((Int, Int)->Int)) -> Int {
        return equation(num1, num2) * 10
    }
    
    //クロージャーを内部で保持する
    func tenTimesWithStore(equation:@escaping ((Int, Int)->Int)) -> Int {
        self.storedEquation = equation
        return equation(num1, num2) * 10
    }
    
    //非同期後に実行する場合
    func tenTimesWithAsync(callback:@escaping ((Int)->Void)) -> Void {
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) { [weak self] in
            guard let myself = self else { return }
            callback((myself.num1 + myself.num2) * 10)
        }
    }
}

class ViewController: UIViewController {
    
    var num3:Int = 2
    var util:MathUtil? = MathUtil(num1: 5, num2: 3)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        test_tenTimes1()
        test_tenTimes2()
        test_tenTimes3()
        test_tenTimesWithStore()
        test_tenTimesWithStore2()
        test_tenTimesWithAsync()
    }
    
    func test_tenTimes1(){
        guard let util = self.util else { return }
        let result = util.tenTimes { (num1:Int, num2:Int) -> Int in
            return num1 + num2
        }
        print("test_tenTimes1 result = \(result)") //result = 80
    }
    
    func test_tenTimes2(){
        guard let util = self.util else { return }
        let result = util.tenTimes { (num1:Int, num2:Int) -> Int in
            return num1 - num2
        }
        print("test_tenTimes2 result = \(result)") //result = 20
    }
    
    func test_tenTimes3(){
        guard let util = self.util else { return }
        //selfをつけていないことに注目
        let result = util.tenTimes { (num1:Int, num2:Int) -> Int in
            return num1 + num2 + num3
        }
        print("test_tenTimes3 result = \(result)") //result = 100
    }
    
    func test_tenTimesWithStore(){
        guard let util = self.util else { return }
        let result = util.tenTimesWithStore { (num1:Int, num2:Int) -> Int in
            return num1 + num2
        }
        print("test_tenTimesWithStore result = \(result)") //result = 80
    }
    
    func test_tenTimesWithStore2(){
        guard let util = self.util else { return }
        
        //内部でselfを使う場合は、weakかunownedで参照する
        let result = util.tenTimesWithStore {
            [unowned self](num1:Int, num2:Int) -> Int in
            
            self.num3 = 1
            return num1 + num2 + self.num3
        }
        print("test_tenTimesWithStore2 result = \(result)") //result = 90
    }
    
    func test_tenTimesWithAsync(){
        guard let util = self.util else { return }
        
        //内部でselfを使う場合は、weakかunownedで参照する
        util.tenTimesWithAsync { [unowned self](result:Int) in
            print("test_tenTimesWithAsync result \(result)") //result = 80
            
            self.num3 = 4
            let result2 = self.num3 + result
            print("test_tenTimesWithAsync result2 \(result2)") //result = 84
            
            self.util = nil
        }
    }
}

出力

test_tenTimes1 result = 80
test_tenTimes2 result = 20
test_tenTimes3 result = 100
test_tenTimesWithStore result = 80
test_tenTimesWithStore2 result = 90
test_tenTimesWithAsync result 80
test_tenTimesWithAsync result2 84
deinit MathUtil
LINEで送る
Pocket

自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

LINEスタンプ作りました!
毎日使える。とぼけたウサギ。LINEスタンプ販売中! 毎日使える。とぼけたウサギ

ページトップへ戻る