この投稿は何?
Swiftにおけるクロージャには、いくつかの属性を指定できます。
その1つに@escaping
がありますが、これについて調査したことを覚書として投稿します。
※ Swift Language Guideより
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「今日からはじめるプログラミング」をお勧めします。
実行環境
- macOS X 10.15.1 Catalina
- Xcode 11.2
クロージャをエスケープする
クロージャをエスケープする例
エスケープクロージャのよくある例として、非同期処理をする完了ハンドラとしてのクロージャがあります。
関数に渡されたハンドラは、開始すると再び関数に戻ります。
ただし、そのクロージャがいつ呼び出されるかは決まっておらず、実際にはハンドラ完了後です。
関数の実行が完了した後にクロージャを呼び出すためには、エスケープしておく必要があります。
@escapeing
属性を記述する
クロージャは、関数に引数として渡されたとき「関数をエスケープ」できます。
エスケープしたクロージャは、関数に戻った後でも呼び出し可能です。
エスケープクロージャを定義するには、関数の引数宣言でクロージャに@escaping
を記述します。
クロージャをエスケープさせるには、関数の外側で定義された変数に割り当てます。
// 関数のスコープ外に定義した変数
var completionHandlers: [() -> Void] = []
// 完了ハンドラとしてエスケープクロージャを受け取る関数
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
// クロージャをスコープ外の変数に割り当てる
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:)
関数は、クロージャを引数として受け取ります。
受け取ったクロージャはすぐに実行せず、関数の外側で宣言された配列に追加しています。
この関数のパラメータを@escaping
でマークしなかった場合、コンパイルエラーが発生します。スコープの外側に割り当てているからです。
一方、次のsomeFunctionWithNonescapingClosure(:)
関数は、クロージャを引数として受け取るものの、エスケープしていません。
ボディでは、受け取ったクロージャをその場で実行するだけです。
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
以下のコードでは、someFunctionWithEscapingClosure(_:)
はエスケープクロージャを受け取ります。
対照的に、someFunctionWithNonescapingClosure(_:)
はエスケープしないクロージャを受け取ります。
class SomeClass {
var x = 10
func doSomething() {
// エスケープしたクロージャなので、selfを明示してプロパティを参照
someFunctionWithEscapingClosure { self.x = 100 }
}
func doAnotherSomething() {
someFunctionWithNonescapingClosure { x = 200 }
}
}
なお、@escaping
でクロージャをマークすると、クロージャ内では自身を明示的に参照する必要があることに注意します。
このクラスのインスタンスからメソッドを実行します。
let instance = SomeClass()
instance.doSomething() // completionHanders配列にハンドラを追加しただけ.
print(instance.x) // xは10のまま.
completionHandlers.first?() // 追加したハンドラが実行されて xが100になる.
print(instance.x) // prints 100
completionHandlers
配列に追加されたクロージャを呼び出すことで、x
の値が100
になります。
instance.doAnotherSomething()
print(instance.x) // prints 200
一方で、エスケープしていないクロージャは、インスタンスメソッド実行時にx
の値に200
を割り当てています。
こんな風に理解した
「クロージャをエスケープする」とはつまり、__元の関数との繋がりを断ち切ること__という感じに理解できるかもしれません。
「やること(クロージャ)だけを決めておいて、いつやるかは後で決める」と理解しました。