先日、窓からさしこむ夏の日差しを眺めながら、とあるLoaderを書いていたのですが、
Escaping closure captures non-escaping parameter 'completion'
というエラーが出ました。
コードをPlaygroundで再現するとこんなんです。
import UIKit
struct Translator {
func translate(word: String) {
let loader = Loader()
loader.load {(string) in
print(string)
}
}
}
struct Loader {
func load(completion: (String) -> Void) {
let request = URLRequest(url: URL(string: "https://qiita.com/")!)
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
// 本当はサーバーからのレスポンスを詰めているがサンプルコードなのでベタ書きする
let string = "Hello"
completion(string) // ここでコンパイルエラー
}
task.resume()
}
}
let translator = Translator()
translator.translate(word: "こんにちは")
対策としては、引数のcompletionに@escaping
をつける、以上なんですが、
つけなきゃいけないケースとつけなくてもいいケースがよくわからないなと思って、調べてみました。
@escaping
の意味
Swift実践入門の説明を引用すると、
escaping属性は、関数に引数として渡されたクロージャが、関数のスコープ外で保持される可能性があることを示す属性です
という説明になっています。
説明が難しい属性なので、こういう表現になるのかなと思いますが、
@escaping
をよく理解していない人間が読んでも、ちょっと抽象的で、わかったようなわからないような……という気持ちになりました。
クロージャーのそもそもの性質をきちんと理解する
@escaping
を正確に理解するためには、下記2つのクロージャーの基本的な性質を理解していることが前提になります。
- クロージャーは自分が定義されたスコープをキャプチャする
- クロージャーは関数の引数として使える
そして、この2つの性質を満たすとき、
関数の引数としてクロージャーを使って、その関数が終わった後に実行されると……?
を考えます。
関数の引数としてクロージャーを使って、その関数が終わった後に実行される
冒頭で紹介したサンプルコードみたいなケースなんですが、ちょっとごちゃついてるので、もう少しシンプルな例で説明したいので、
公式サンプルを引っ張ってきました。
var completionHandlers = [() -> Void]()
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
completionHandlers
がクロージャーの配列になっており、関数someFunctionWithEscapingClosure(_:)
は引数で受け取ったクロージャーをそこにappendしていきます。
実行はこのコードの中だとまだ行われていません。
どこで行われるでしょうか?
配列completionHandlers
にアクセスできる場所ならどこでも実行できますね。
何が問題か?
引数にクロージャーがあるからといって、常に@escaping
が必要なわけではありません。
むしろ必要なケースの方が少ないでしょう。
たとえば、下記は不要な例です。
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
クロージャーの実行が関数内のスコープで完結しているので、問題がありません。
では関数の外で実行されると何が問題なのでしょうか?
簡単ながら図示してみました。
問題がないsomeFunctionWithNonescapingClosure()
は、Scope1/Scope2で処理が完結しています。
someFunctionWithNonescapingClosure()と引数のclosureは別スコープで定義されていますが、
関数の実行中はクロージャー及びその親スコープの状態が不変であることが保証されています。
ところが、someFunctionWithEscapingClosure()
の方は、配列completionHandlers
を実行する、別のScope3が存在します。
クロージャーは自分が定義されたスコープをキャプチャするので、Scope1への参照ができる必要があります。
しかし図のケースだと、Scope3で実行されるときに、Scope1が必ず解放されていない保証はありません。
// Scope 2
var completionHandlers = [() -> Void]()
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
// Scope 1
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
}
}
// Scope 3(この例だとScope 2と同じだけど、@escapingないとコンパイラに怒られる)
let instance = SomeClass()
instance.doSomething()
completionHandlers.first?()
print(instance.x)
// Prints "100"
クロージャーの中の書き方によっては、循環参照を起こす危険性があるので、クロージャーの中で親スコープを参照するときはself
をつけることになります。
クロージャーをescapeさせる必要があるシチュエーションってなんだ?
で、今回たまたま仕事でLoader書いてて@escapingつけなきゃいけないシーンにぶち当たったんですが、
そもそもクロージャーをescapeさせる必要があるシチュエーションって現実的になんでしょう?
swiftでクロージャーに@escapingをつける場合を調べてみたを読むと、
- クロージャーがプロパティとして保存される(強参照される)
- クロージャーがメソッド内ですぐに実行されない(非同期である)
ということなんですが、実際そんな設計にしますかね?
Swiftの公式サンプルもクロージャーの配列なんてつくってますが、実際そんなの書きたいときありますか?
そもそも最初に書いたサンプルのURLSession
はなぜ怒られたんでしょう?
そんなことを考えていくと、一つの答えにたどりつきました。
処理をキューイングさせたいケースでは、クロージャーを配列で保持するコード設計になるかと思います。
具体的には、下記のようなケース。
- OperationQueueを使ってマルチスレッドでタスクを実行したい
- URLSessionを使ってタスクを並列で実行したい
- タイマーで所定の時間にいくつかのタスクの実行を予約したい
URLSession
もよく見ると、delegateQueue
というタスクをキューイングするプロパティを持っていて、
クロージャーでの実行もおそらくこっちに投入されるのではないかと思われます。
というか3つ例を出しましたけど、つまるところ複数タスクを非同期で並列処理させたいときですね。
まとめ
わかりやすく書きたいと思って書いたのですが、わかりやすいかは微妙です……
何かありましたら、お気軽にコメント・編集リクエストくださいー
環境
Swift 5.2.4。
Swift2以下だと、クロージャーのデフォルトが@escaping
だったとか。
参考