Swift 言語ガイド – 7. クロージャ

Swift

はじめに

公式ページのSwift 言語ガイド「The Swift Programming Language Swift 5.3」に基づき記載いたします。本ページでは、「クロージャ」について記載いたします。
記載内容に誤り等ございましたら、ご連絡をいただければ幸いです。

7. クロージャ

クロージャは、コード内で受け渡して使用できる機能の自己完結型のブロックです。Swiftのクロージャは、CおよびObjective-Cのブロック、および他のプログラミング言語のラムダに似ています。

クロージャは、定数および変数への参照を、それらが定義されているコンテキストからキャプチャして保存できます。これは、これらの定数と変数を締めくくることとして知られています。Swiftは、キャプチャのすべてのメモリ管理を処理します。

注意
キャプチャの概念に慣れていなくても心配しないでください。これについては、以下の「値のキャプチャ」で詳しく説明します。

関数で導入されたグローバル関数とネストされた関数は、実際にはクロージャの特殊なケースです。クロージャは、次の3つの形式のいずれかを取ります。

  • グローバル関数は、名前があり、値をキャプチャしないクロージャです。
  • ネストされた関数は、名前があり、囲んでいる関数から値を取得できるクロージャです。
  • クロージャ式は、周囲のコンテキストから値をキャプチャできる軽量の構文で記述された名前のないクロージャです。

Swiftのクロージャ式は、一般的なシナリオで簡潔で混乱のない構文を促進する最適化を備えた、すっきりとした明確なスタイルを備えています。これらの最適化には次のものが含まれます。

  • コンテキストからパラメータと戻り値のタイプを推測する
  • 単一式クロージャからの暗黙的なリターン
  • 省略引数名
  • 末尾のクロージャ構文

7.1. クロージャ式

ネストされた関数で導入されたネストされた関数は、より大きな関数の一部として自己完結型のコードブロックに名前を付けて定義する便利な手段です。ただし、完全な宣言と名前を付けずに、関数のような構造の短いバージョンを作成すると便利な場合があります。これは、1つ以上の引数を取る関数またはメソッドを使用する場合に特に当てはまります。

クロージャ式は、簡潔で焦点を絞った構文でインラインクロージャを記述する方法です。クロージャ式は、明確さや意図を失うことなく、短縮された形式でクロージャを記述するためのいくつかの構文最適化を提供します。以下のクロージャ式の例は、sorted(by:)メソッドの1つの例を複数の反復にわたって改良することにより、これらの最適化を示しています。各反復は、同じ機能をより簡潔に表現しています。

Swiftの標準ライブラリは、sorted(by:)と呼ばれるメソッドを提供します。このメソッドは、指定した並べ替えクロージャの出力に基づいて、既知の型の値の配列を並べ替えます。並べ替えプロセスが完了すると、sorted(by:)メソッドは、古い配列と同じタイプとサイズの新しい配列を返し、その要素は正しい並べ替え順序になります。元の配列は、sorted(by:)メソッドによって変更されません。

以下のクロージャ式の例では、sorted(by:)メソッドを使用して、文字列値の配列をアルファベットの逆順で並べ替えています。並べ替える初期配列は次のとおりです。

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sorted(by:)メソッドは、配列の内容と同じ型の2つの引数を取るクロージャを受け入れ、値が並べ替えられた後、最初の値が2番目の値の前に表示されるか後に表示されるかを示すBool値を返します。ソートクロージャは、最初の値が2番目の値の前に表示される場合はtrueを返し、それ以外の場合はfalseを返す必要があります。

この例では、文字列値の配列を並べ替えているため、並べ替えクロージャは(String, String) -> Bool型の関数である必要があります。

ソートクロージャを提供する1つの方法は、正しい型の通常の関数を記述し、それを引数としてsorted(by:)メソッドに渡すことです。

func backly(_ s1:String、_ s2:String)-> Bool {
    s1> s2を返します
}
var reverseNames = names.sorted(by:backward)
// reverseNamesは["Ewa"、 "Daniella"、 "Chris"、 "Barry"、 "Alex"]と同じです

最初の文字列(s1)が2番目の文字列(s2)より大きい場合、backward(_:_:)関数はtrueを返し、ソートされた配列でs1がs2の前に表示される必要があることを示します。文字列内の文字の場合、「より大きい」は「アルファベットの後半に現れる」を意味します。これは、文字「B」が文字「A」より「大きい」ことを意味し、文字列「Tom」が文字列「Tim」よりも大きいことを意味します。これにより、アルファベットの逆ソートが行われ、「Barry」が「Alex」の前に配置されます。

ただし、これは本質的に単一式の関数(a > b)を記述するためのかなり長い方法です。この例では、クロージャ式の構文を使用して、ソートクロージャをインラインで記述することが望ましいでしょう。

7.2. クロージャ式の構文

クロージャ式の構文には、次の一般的な形式があります。

{ (パラメータ) -> 戻り値の型 in
    ステートメント
}

クロージャ式構文のパラメータは、入出力パラメータにすることができますが、デフォルト値を持つことはできません。可変個引数パラメータに名前を付けると、可変個引数パラメータを使用できます。タプルは、パラメータの型および戻り値の型としても使用できます。

以下の例は、上からのbackward(_:_:)関数のクロージャ式バージョンを示しています。

reverseNames = names.sorted(by:{(s1:String、s2:String)-> Bool in
    s1> s2を返します
})

このインラインクロージャのパラメータと戻り値の型の宣言は、backward(_:_:)関数からの宣言と同じであることに注意してください。どちらの場合も、(s1:String, s2:String) -> Boolと記述されます。ただし、インラインクロージャ式の場合、パラメータと戻り値の型は中括弧の外側ではなく、中括弧の内側に記述されます。

クロージャの本体の開始は、inキーワードによって導入されます。このキーワードは、クロージャのパラメータと戻り値の型の定義が終了し、クロージャの本体がまもなく開始されることを示します。

クロージャーの本体は非常に短いため、1行で書くこともできます。

reverseNames = names.sorted(by:{(s1:String、s2:String)-> Bool in return s1> s2})

これは、sorted(by:)メソッドの全体的な呼び出しが同じままであることを示しています。括弧のペアは、メソッドの引数全体をラップします。ただし、その引数は現在、インラインクロージャです。

7.3. コンテキストから型推測

ソートクロージャはメソッドへの引数として渡されるため、Swiftはそのパラメータの型と返す値の型を推測できます。sorted(by:)メソッドは文字列の配列で呼び出されているため、その引数は(String, String) -> Bool型の関数である必要があります。つまり、(String, String)型とBool型は、クロージャ式の定義の一部として記述する必要はありません。すべての型を推測できるため、戻り矢印(->)とパラメータ名を囲む括弧も省略できます。

reverseNames = names.sorted(by:{s1、s2 in return s1> s2})

クロージャを関数またはメソッドにインラインクロージャ式として渡す場合、パラメータ型と戻り値の型を推測することは常に可能です。その結果、クロージャが関数またはメソッドの引数として使用される場合、完全な形式でインラインクロージャを記述する必要はありません。

それでも、必要に応じて型を明示的にすることができます。コードの読者のあいまいさを回避できる場合は、そうすることをお勧めします。 sorted(by:)メソッドの場合、ソートが行われているという事実からクロージャの目的は明らかであり、読者はクロージャーが文字列値で機能している可能性が高いと想定するのが安全です。文字列の配列の並べ替えを支援しているからです。

7.4. 単一式クロージャからの暗黙のリターン

前の例のこのバージョンのように、単一式のクロージャは、宣言からreturnキーワードを省略することにより、単一の式の結果を暗黙的に返すことができます。

reverseNames = names.sorted(by:{s1、s2 in s1> s2})

ここで、sorted(by:)メソッドの引数の型は、ブール値がクロージャによって返される必要があることを明確にしています。クロージャの本体には、ブール値を返す単一の式(s1 > s2)が含まれているため、あいまいさはなく、returnキーワードは省略できます。

7.5. 短縮引数名

Swiftは、インラインクロージャに短縮引数名を自動的に提供します。これを使用して、クロージャの引数の値を$0、$1、$2などの名前で参照できます。

クロージャ式内でこれらの短縮引数名を使用する場合、その定義からクロージャの引数リストを省略できます。短縮引数名の数と型は、予想される関数型から推測されます。クロージャ式は完全に本体で構成されているため、inキーワードは省略できます。

reverseNames = names.sorted(by:{$ 0> $ 1})

ここで、$0と$1は、クロージャの最初と2番目の文字列引数を指します。

7.6. 演算子メソッド

実際には、上記のクロージャ式を記述するさらに短い方法があります。SwiftのString型は、大なり演算子(>)の文字列固有の実装を、String型の2つのパラメータを持ち、Bool型の値を返すメソッドとして定義します。これは、sorted(by:)メソッドに必要なメソッド型と完全に一致します。したがって、大なり記号演算子を渡すだけで、Swiftは文字列固有の実装を使用することを推測します。

reverseNames = names.sorted(by:>)

演算子メソッドの詳細については、「演算子メソッド」を参照してください。

7.7. トレーリングクロージャ

関数の最後の引数としてクロージャ式を関数に渡す必要があり、クロージャ式が長い場合は、代わりにトレーリングクロージャとして記述すると便利です。トレーリングクロージャはまだ関数の引数ですが、関数呼び出しの括弧の後にトレーリングクロージャを記述しますトレーリングクロージャ構文を使用する場合、関数呼び出しの一部として最初のクロージャの引数ラベルを記述しません。関数呼び出しには、複数のトレーリングクロージャを含めることができます。ただし、以下の最初のいくつかの例では、単一のトレーリングクロージャを使用しています。

func someFunctionThatTakesAClosure(closure:()-> Void){
    //関数本体はここにあります
}

//末尾のクロージャを使用せずにこの関数を呼び出す方法は次のとおりです。

someFunctionThatTakesAClosure(closure:{
    //クロージャーのボディはここにあります
})

//代わりに末尾のクロージャを使用してこの関数を呼び出す方法は次のとおりです。

someFunctionThatTakesAClosure(){
    //末尾のクロージャの本体はここにあります
}

上記のクロージャ式構文セクションの文字列ソートクロージャは、sorted(by:)メソッドの括弧の外側にトレーリングクロージャとして記述できます。

reverseNames = names.sorted(){$ 0> $ 1}

クロージャ式が関数またはメソッドの唯一の引数として提供され、その式をトレーリングクロージャとして指定する場合、関数を呼び出すときに、関数またはメソッドの名前の後に括弧()のペアを記述する必要はありません。

reverseNames = names.sorted {$ 0> $ 1}

トレーリングクロージャは、クロージャが十分に長く、1行にインラインで書き込むことができない場合に最も役立ちます。例として、Swiftの配列型にはmap(_:)メソッドがあり、単一の引数としてクロージャ式を取ります。クロージャは、配列内のアイテムごとに1回呼び出され、そのアイテムの代替のマップされた値(おそらく他の型のもの)を返します。 map(_:)に渡すクロージャにコードを記述することにより、マッピングの性質と戻り値の型を指定します。

提供されたクロージャを各配列要素に適用した後、map(_:)メソッドは、元の配列の対応する値と同じ順序で、すべての新しいマップされた値を含む新しい配列を返します。

末尾のクロージャでmap(_:)メソッドを使用して、Int値の配列をString値の配列に変換する方法は次のとおりです。配列[16、58、510]は、新しい配列[“OneSix”、 “FiveEight”、 “FiveOneZero”]を作成するために使用されます。

let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

上記のコードは、整数桁とその名前の英語バージョンの間のマッピングの辞書を作成します。また、文字列に変換する準備ができている整数の配列も定義します。

クロージャ式を配列のmap(_:)メソッドにトレーリングクロージャとして渡すことにより、numbers配列を使用して文字列値の配列を作成できるようになりました。

let strings = numbers.map { (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// 文字列は[String]型であると推測されます
// その値は["OneSix"、 "FiveEight"、 "FiveOneZero"]です

map(_:)メソッドは、配列内のアイテムごとにクロージャ式を1回呼び出します。マップする配列の値から型を推測できるため、クロージャの入力パラメータの型である数値を指定する必要はありません。

この例では、変数numberはクロージャのnumberパラメーターの値で初期化されるため、クロージャ本体内で値を変更できます。(関数とクロージャのパラメータは常に定数です。)クロージャ式は、マップされた出力配列に格納される型を示すために、文字列の戻り値の型も指定します。

クロージャ式は、呼び出されるたびにoutputという文字列を作成します。剰余演算子(数値%10)を使用して数値の最後の桁を計算し、この桁を使用してdigitNamesディクショナリで適切な文字列を検索します。クロージャを使用して、ゼロより大きい任意の整数の文字列表現を作成できます。

注意
辞書の添え字は、キーが存在しない場合に辞書のルックアップが失敗する可能性があることを示すオプショナルの値を返すため、digitNames辞書の添え字の呼び出しの後に感嘆符(!)が続きます。上記の例では、数値%10が常にdigitNames辞書の有効な添え字キーであることが保証されているため、感嘆符を使用して、添え字のオプショナルの戻り値に格納されている文字列値を強制的にアンラップします。

DigitNames辞書から取得した文字列が出力の前に追加され、数値の文字列バージョンが逆に効果的に作成されます。(式番号%10は、16の場合は6、58の場合は8、510の場合は0の値を示します。)

次に、数値変数は10で除算されます。整数であるため、除算中に切り捨てられます。したがって、16は1になり、58は5になり、510は51になります。

このプロセスは、数値が0に等しくなるまで繰り返されます。その時点で、出力文字列がクロージャによって返され、map(_:)メソッドによって出力配列に追加されます。

上記の例でトレーリングクロージャ構文を使用すると、クロージャがサポートする関数の直後にクロージャの機能が適切にカプセル化されます。クロージャ全体をmap(_:)メソッドの外側の括弧で囲む必要はありません。

関数が複数のクロージャを取る場合は、最初のトレーリングクロージャの引数ラベルを省略し、残りのトレーリングクロージャにラベルを付けます。たとえば、次の関数はフォトギャラリーの画像を読み込みます。

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

この関数を呼び出して画像をロードするときは、2つのクロージャを提供します。最初のクロージャは、ダウンロードが成功した後に画像を表示する完了ハンドラーです。 2番目のクロージャは、ユーザーにエラーを表示するエラーハンドラーです。

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

この例では、loadPicture(from:completion:onFailure:)関数がネットワークタスクをバックグラウンドにディスパッチし、ネットワークタスクが終了すると2つの完了ハンドラーのいずれかを呼び出します。このように関数を作成すると、両方の状況を処理する1つのクロージャを使用する代わりに、ネットワーク障害の処理を担当するコードを、ダウンロードが成功した後にユーザーインターフェイスを更新するコードから明確に分離できます。

7.8. 値のキャプチャ

クロージャは、それが定義されている周囲のコンテキストから定数と変数をキャプチャできます。クロージャは、定数と変数を定義した元のスコープが存在しなくなった場合でも、本体内からそれらの定数と変数の値を参照および変更できます。

Swiftでは、値をキャプチャできるクロージャの最も単純な形式は、別の関数の本体内に記述されたネストされた関数です。ネストされた関数は、その外部関数の引数をキャプチャでき、外部関数内で定義された定数や変数もキャプチャできます。

これは、makeIncrementerと呼ばれる関数の例です。これには、incrementerと呼ばれるネストされた関数が含まれています。ネストされたincrementer()関数は、周囲のコンテキストから、runningTotalとamountの2つの値をキャプチャします。これらの値をキャプチャした後、インクリメントは、呼び出されるたびにrunningTotalを量だけインクリメントするクロージャとしてmakeIncrementerによって返されます。

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

makeIncrementerの戻り値の型は() -> Intです。これは、単純な値ではなく、関数を返すことを意味します。返される関数にはパラメータがなく、呼び出されるたびにInt値を返します。関数が他の関数を返す方法については、戻り値の型としての関数の型を参照してください。

makeIncrementer(forIncrement:)関数は、runningTotalと呼ばれる整数変数を定義して、返されるインクリメンターの現在の合計を格納します。この変数は値0で初期化されます。

makeIncrementer(forIncrement:)関数には、forIncrementの引数ラベルとamountのパラメータ名を持つ単一のIntパラメータがあります。このパラメータに渡される引数値は、返されたincrementer関数が呼び出されるたびに、runningTotalをインクリメントする量を指定します。makeIncrementer関数は、実際のインクリメントを実行するincrementerと呼ばれるネストされた関数を定義します。この関数は単にrunningTotalに金額を加算し、結果を返します。

分けて検討すると、ネストされたincrementer()関数は異常に見える場合があります。

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

incrementer()関数にはパラメータがありませんが、関数本体内からrunningTotalとamountを参照します。これは、runningTotalとamountへの参照を周囲の関数からキャプチャし、それらを独自の関数本体内で使用することによって行われます。参照によるキャプチャにより、makeIncrementerの呼び出しが終了したときにrunningTotalとamountが消えないようにし、次にincrementer関数が呼び出されたときにrunningTotalが使用可能になるようにします。

注意
最適化として、Swiftは、値がクロージャによって変更されていない場合、およびクロージャの作成後に値が変更されていない場合、代わりに値のコピーをキャプチャして保存する場合があります。

Swiftは、変数が不要になったときに変数を破棄することに関連するすべてのメモリ管理も処理します。

makeIncrementerの動作例を次に示します。

let incrementByTen = makeIncrementer(forIncrement: 10)

この例では、incrementByTenという定数を設定して、呼び出されるたびにrunningTotal変数に10を追加するインクリメンター関数を参照します。関数を複数回呼び出すと、この動作が実際に動作していることがわかります。

incrementalByTen()
// 10の値を返します
incrementalByTen()
// 20の値を返します
incrementalByTen()
// 30の値を返します

2番目のインクリメントを作成すると、新しい個別のrunningTotal変数への独自の参照が格納されます。

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 7の値を返します

元のインクリメンター(incrementByTen)を再度呼び出すと、それ自体のrunningTotal変数が引き続きインクリメントされ、incrementBySevenによってキャプチャされた変数には影響しません。

incrementalByTen()
// 40の値を返します

注意
クラスインスタンスのプロパティにクロージャを割り当て、クロージャがインスタンスまたはそのメンバーを参照してそのインスタンスをキャプチャする場合、クロージャとインスタンスの間に強力な参照サイクルが作成されます。Swiftは、キャプチャリストを使用して、これらの強力な参照サイクルを中断します。詳細については、「クロージャの強力な参照サイクル」を参照してください。

7.9. クロージャは参照型

上記の例では、incrementBySevenとincrementByTenは定数ですが、これらの定数が参照するクロージャは、キャプチャしたrunningTotal変数を引き続きインクリメントできます。これは、関数とクロージャが参照型であるためです。

関数またはクロージャを定数または変数に割り当てるときはいつでも、実際にはその定数または変数を関数またはクロージャへの参照として設定しています。上記の例では、incrementByTenが参照するのはクロージャの選択であり、クロージャ自体の内容ではなく一定です。

これは、2つの異なる定数または変数にクロージャを割り当てる場合、それらの定数または変数の両方が同じクロージャを参照することも意味します。

alsoIncrementByTen = incrementalByTen
alsoIncrementByTen()
// 50の値を返します

incrementalByTen()
// 60の値を返します

上記の例は、alsoIncrementByTenを呼び出すことはincrementByTenを呼び出すことと同じであることを示しています。どちらも同じクロージャを参照しているため、両方ともインクリメントして同じ現在の合計を返します。

7.10. クロージャのエスケープ

クロージャは、クロージャが関数の引数として渡されたときに関数をエスケープすると言われますが、関数が戻った後に呼び出されます。パラメータの1つとしてクロージャを受け取る関数を宣言する場合、パラメータの型の前に@escapingを記述して、クロージャがエスケープできることを示すことができます。

クロージャをエスケープする1つの方法は、関数の外部で定義された変数に格納することです。例として、非同期操作を開始する多くの関数は、完了ハンドラーとしてクロージャ引数を取ります。関数は操作の開始後に戻りますが、操作が完了するまでクロージャは呼び出されません。クロージャはエスケープして、後で呼び出す必要があります。例えば:

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

someFunctionWithEscapingClosure(_:)関数は、引数としてクロージャを取り、関数の外部で宣言されている配列に追加します。この関数のパラメーターを@escapingでマークしなかった場合、コンパイル時エラーが発生します。

自己がクラスのインスタンスを参照する場合、自己を参照するエスケープクロージャには特別な考慮が必要です。エスケープクロージャで自己をキャプチャすると、誤って強力な参照サイクルを作成しやすくなります。参照サイクルについては、自動参照カウントを参照してください。

通常、クロージャは、クロージャの本体で変数を使用して暗黙的に変数をキャプチャしますが、この場合は明示的にする必要があります。自己をキャプチャする場合は、使用するときに明示的に自己を記述するか、クロージャのキャプチャリストに自己を含めます。自己を明示的に書くことで、意図を表現でき、参照サイクルがないことを確認するように促されます。たとえば、以下のコードでは、someFunctionWithEscapingClosure(_:)に渡されるクロージャは自己を明示的に参照しています。対照的に、someFunctionWithNonescapingClosure(_:)に渡されるクロージャは、エスケープなしのクロージャです。つまり、暗黙的に自己を参照できます。

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// "200"を出力します

completionHandlers.first?()
print(instance.x)
// "100"を出力します

これは、クロージャーのキャプチャリストに含めることで自己をキャプチャし、暗黙的に自己を参照するdoSomething()のバージョンです。

class SomeOtherClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { [self] in x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

selfが構造体または列挙型のインスタンスである場合、いつでも暗黙的にselfを参照できます。ただし、selfが構造体または列挙型のインスタンスである場合、エスケープクロージャはselfへの変更可能な参照をキャプチャできません。構造体と列挙型は値型であるで説明されているように、構造体と列挙型では共有の可変性は許可されていません。

struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

上記の例のsomeFunctionWithEscapingClosure関数の呼び出しは、変更メソッド内にあるためエラーであり、selfは変更可能です。これは、エスケープするクロージャは構造体の自己への変更可能な参照をキャプチャできないという規則に違反します。

7.11. オートクロージャ

オートクロージャは、関数の引数として渡される式をラップするために自動的に作成されるクロージャです。引数をとらず、呼び出されると、その中にラップされている式の値を返します。この構文上の利便性により、明示的なクロージャの代わりに通常の式を記述することで、関数のパラメータを中括弧で囲むことができます。

オートクロージャを行う関数を呼び出すことは一般的ですが、そのような関数を実装することは一般的ではありません。たとえば、assert(condition:message:file:line:)関数は、その条件とメッセージパラメータのオートクロージャを取ります。その条件パラメータはデバッグビルドでのみ評価され、そのメッセージパラメータは条件がfalseの場合にのみ評価されます。

オートクロージャを使用すると、クロージャを呼び出すまで内部のコードが実行されないため、評価を遅らせることができます。評価の遅延は、コードがいつ評価されるかを制御できるため、副作用があるコードや計算コストが高いコードに役立ちます。以下のコードは、クロージャが評価をどのように遅らせるかを示しています。

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// "5"を出力します

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// "5"を出力します

print("Now serving \(customerProvider())!")
// "Now serving Chris!"を出力します
print(customersInLine.count)
// "4"を出力します

CustomersInLine配列の最初の要素はクロージャ内のコードによって削除されますが、配列要素はクロージャが実際に呼び出されるまで削除されません。クロージャが呼び出されない場合、クロージャ内の式が評価されることはありません。つまり、配列要素が削除されることはありません。 customerProviderの型は文字列ではなく、() -> String – 文字列を返すパラメータのない関数であることに注意してください。

関数の引数としてクロージャを渡すと、遅延評価と同じ動作が得られます。

// customersInLineは["Alex", "Ewa", "Barry", "Daniella"]です
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// "Now serving Alex!"を出力します

上記のリストのserve(customer:)関数は、顧客の名前を返す明示的なクロージャを取ります。以下のバージョンのserve(customer:)は同じ操作を実行しますが、明示的なクロージャを取得する代わりに、パラメータの型を@autoclosure属性でマークすることによってオートクロージャを取得します。これで、クロージャの代わりにString引数をとったかのように関数を呼び出すことができます。 customerProviderパラメータの型は@autoclosure属性でマークされているため、引数は自動的にクロージャに変換されます。

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

注意
オートクロージャを使いすぎると、コードが理解しにくくなる可能性があります。コンテキストと関数名は、評価が延期されていることを明確にする必要があります。

エスケープを許可するオートクロージャが必要な場合は、@autoclosure属性と@escaping属性の両方を使用します。@escaping属性については、上記の「クロージャのエスケープ」で説明しています。

// customersInLineは["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())!")
}
// "Now serving Barry!"を出力します
// "Now serving Daniella!"を出力します

上記のコードでは、customerProvider引数として渡されたクロージャを呼び出す代わりに、collectCustomerProviders(_:)関数がクロージャをcustomerProviders配列に追加します。配列は関数のスコープ外で宣言されます。つまり、配列のクロージャは関数が戻った後に実行できます。その結果、customerProvider引数の値は、関数のスコープをエスケープできるようにする必要があります。

タイトルとURLをコピーしました