Swiftを学ぶ過程でescapingというキーワードに出会うことがあると思います。
今回は、クロージャのescapingについて以下の観点で丁寧も解説します。
- Swiftのクロージャのescapingとはなにか
- Swiftのクロージャのescapingはどのようなときに使うか
- Swiftのクロージャのescapingの使い方
参考本
[改訂新版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plus
Swiftのクロージャのescapingとはなにか
クロージャが引数として関数に渡され、関数がリターンした後にそのクロージャが呼び出される場合、クロージャをエスケープすると言います。
クロージャをエスケープする場合、以下のようにクロージャの型の前に@escapingをつけます。
closure()
}
なお、@escapingをつけない場合、クロージャはエスケープされません。
なので、以下のようにクロージャが退避される場合、@escapingをつけないとコンパイルエラーになります。
func someFunctionWithEscapingClosure(completionHandler: () -> Void) {
completionHandlers.append(completionHandler)
}
なお、var completionHandlers: [() -> Void] = []の() -> Voidは関数型になります。
関数型については
Swiftの関数型
を参照してください。
Swiftのクロージャのescapingはどのようなときに使うか
それでは、どのようなときクロージャをエスケープさせたいでしょうか。
もっとも多いのは非同期処理をしたい場合です。
上の例の場合、someFunctionWithEscapingClosureが処理されると関数はリターンしますが、
completionHandleはcompletionHandlers.append(completionHandler)で退避されているので、
関数の処理を完了してもクロージャは呼び出されず、呼び出すタイミングをずらすことができます。
Swiftのクロージャのescapingの使い方
それでは、Swiftのクロージャのescapingの具体的な使い方について見ていきましょう。
まず、下の例を確認してください。
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeType {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure {
self.x = 12
print(self.x)
}
someFunctionWithNonescapingClosure {
print(x)
}
}
}
let instance = SomeType()
instance.doSomething()
//10と出力
completionHandlers.first?()
//12と出力
someFunctionWithEscapingClosureやomeFunctionWithNonescapingClosureでは後置クロージャとしてクロージャの本体を記述しています。
後置クロージャについては
Swiftのクロージャ
を参照してください。
さて、instance.doSomething()を実行したとき
someFunctionWithNonescapingClosureが機能して
print(x)でSomeTypeのxがキャプチャされて出力されています。
しかし、someFunctionWithEscapingClosureは機能していません。
なぜなら、someFunctionWithEscapingClosureではcompletionHandlerがcompletionHandlersに退避されているからです。
そして、completionHandlers.first?()を実行したとき初めてcompletionHandlerが機能しています。
このように、クロージャをエスケープすることで実行のタイミングをコントロールすることができます。
それから、通常、クロージャは、暗黙的にコンテキストの変数や定数の値をキャプチャしますが、
エスケープする場合、実行環境のことを考慮して
「自身の」コンテキストの変数や定数の値をキャプチャする
ということを明示する必要があります。
なので、self.xのようにキャプチャする対象にselfをつけるようにします。
さて、ここで、以下のようにクラスを構造体に変えるとどうなるでしょうか。
var x = 10
func doSomething() {
someFunctionWithEscapingClosure {
self.x = 12
print(self.x)
}
someFunctionWithNonescapingClosure {
print(x)
}
}
}
そうすると、self.x = 12に対して「immutableなのでプロパティに値を割り当てられません」というエラーが発生します。
つまり、クラスはmutableな参照型で、構造体はデータの値と同様、immutableな値型だということです。
なお、関数も含めてクロージャは参照型、列挙型(Enumerations)は値型になります。
さて、クロージャをエスケープするときに一緒に使える便利な方法があります。
それは、自動クロージャ(autoclousure)です。
自動クロージャとは、通常の式から自動的にクロージャを生成するSwiftの機能です。
以下の例を見てください。
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders({customersInLine.remove(at: 0)})
collectCustomerProviders({customersInLine.remove(at: 0)})
print(“Collected \(customerProviders.count) closures.”)
// Prints “Collected 2 closures.”
for customerProvider in customerProviders {
print(“Now serving \(customerProvider())!”)
}
// Prints “Now serving Barry!”
// Prints “Now serving Daniella!”
{customersInLine.remove(at: 0)}は通常のクロージャ式に則って記述しています。
このような簡単な式の場合、わざわざ{}(ブレース)を使ってクロージャ式にしなくても、以下のように@autoclosureを使うことで普通の式として関数に渡すことができます。
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
print(“Collected \(customerProviders.count) closures.”)
// Prints “Collected 2 closures.”
for customerProvider in customerProviders {
print(“Now serving \(customerProvider())!”)
}
// Prints “Now serving Barry!”
// Prints “Now serving Daniella!”
この場合、customersInLine.remove(at: 0)という通常の式が@autoclosureによってクロージャに変換されています。
以上、今回はSwiftのクロージャのエスケープについて解説しました。