こんにちは。きんくまです。
自分でクロージャーを使ったメソッドを作りたかったときに、@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をつけた方がよいです。
つけないと循環参照になってしまい、メモリーリークしてしまいます。
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
■ 自作iPhoneアプリ 好評発売中!
・フォルメモ - シンプルなフォルダつきメモ帳
・ジッピー電卓 - 消費税や割引もサクサク計算!
■ LINEスタンプ作りました!
毎日使える。とぼけたウサギ