楽水

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

Swift

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

投稿日:2020年8月26日 更新日:


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
-, , ,

執筆者:

関連記事

Swiftのメソッドをわかりやすく解説

メソッドは、特定の型に関連した関数です。 クラス、構造体、列挙型はすべて、型のインスタンスを扱う特定のタスクと機能を持つインスタンスメソッドを定義することができます。 また、クラス、構造体、列挙型は、 …

Swiftのオプショナル【?や!をわかりやすく解説】

Javaでプログラムを書いたことがあるプログラマーであれば「NullPointerException」というエラーで悩まされた方もいらっしゃるのではないでしょうか。 これは、オブジェクトに対する参照が …

Swiftの関数のパラメータ【引数ラベルなどについてわかりやすく解説】

Swift 関数には、パラメータ名の無いシンプルな関数から、引数ラベルや各種パラメータがあるメソッドまで表現できる柔軟性があります。 パラメータは、関数の呼び出しを簡略化するためのデフォルト値を持つこ …

Swiftのデイニシャリゼーションについてわかりやすく解説

Swiftでは、インスタンスが必要でなくなったとき、リソースを解放するために、自動参照カウント (ARC) という方法でインスタンスの割り当てを解除します。 インスタンスの割り当て解除が行わ …

Swiftの関数のネストについてわかりやすく解説

Swiftでは、関数のスコープ内に便利な機能を包むために、他の関数の中に関数を記述することができます。 ネストされた関数を別のスコープで使用できるように、ネストしている関数からネストされた関数を返すこ …