楽水

人々の創造が自由に表現できる舞台づくり

Swift

Swiftのクロージャのエスケープ【escapingについてわかりやすく解説】

投稿日:


Swiftを学ぶ過程でescapingというキーワードに出会うことがあると思います。
今回は、クロージャのescapingについて以下の観点で丁寧も解説します。

  • Swiftのクロージャのescapingとはなにか
  • Swiftのクロージャのescapingはどのようなときに使うか
  • Swiftのクロージャのescapingの使い方

参考本
[改訂新版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plus

Swiftのクロージャのescapingとはなにか

クロージャが引数として関数に渡され、関数がリターンした後にそのクロージャが呼び出される場合、クロージャをエスケープすると言います。
クロージャをエスケープする場合、以下のようにクロージャの型の前に@escapingをつけます。

func someFunctionWithEscapingClosure(closure: @escaping () -> Void) {
closure()
}

なお、@escapingをつけない場合、クロージャはエスケープされません。
なので、以下のようにクロージャが退避される場合、@escapingをつけないとコンパイルエラーになります。

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: () -> Void) {
completionHandlers.append(completionHandler)
}

なお、var completionHandlers: [() -> Void] = []の() -> Voidは関数型になります。
関数型については
Swiftの関数型
を参照してください。

Swiftのクロージャのescapingはどのようなときに使うか

それでは、どのようなときクロージャをエスケープさせたいでしょうか。
もっとも多いのは非同期処理をしたい場合です。
上の例の場合、someFunctionWithEscapingClosureが処理されると関数はリターンしますが、
completionHandleはcompletionHandlers.append(completionHandler)で退避されているので、
関数の処理を完了してもクロージャは呼び出されず、呼び出すタイミングをずらすことができます。

Swiftのクロージャのescapingの使い方

それでは、Swiftのクロージャのescapingの具体的な使い方について見ていきましょう。
まず、下の例を確認してください。

var completionHandlers = [() -> Void]()
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をつけるようにします。
さて、ここで、以下のようにクラスを構造体に変えるとどうなるでしょうか。

struct SomeType {
 var x = 10
 func doSomething() {
  someFunctionWithEscapingClosure {
   self.x = 12
   print(self.x)
  }
  someFunctionWithNonescapingClosure {
   print(x)
  }
 }
}

そうすると、self.x = 12に対して「immutableなのでプロパティに値を割り当てられません」というエラーが発生します。
つまり、クラスはmutableな参照型で、構造体はデータの値と同様、immutableな値型だということです。
なお、関数も含めてクロージャは参照型、列挙型(Enumerations)は値型になります。
さて、クロージャをエスケープするときに一緒に使える便利な方法があります。
それは、自動クロージャ(autoclousure)です。
自動クロージャとは、通常の式から自動的にクロージャを生成するSwiftの機能です。
以下の例を見てください。

// customersInLine is [“Barry”, “Daniella”]
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を使うことで普通の式として関数に渡すことができます。

// customersInLine is [“Barry”, “Daniella”]
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のクロージャのエスケープについて解説しました。

-Swift
-, , ,

執筆者: