見出し画像

Swiftでプログラミング。 - Closures 3

Closures Are Reference Types   参照型クロージャ

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

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

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

   let alsoIncrementByTen = incrementByTen
   alsoIncrementByTen()
   // returns a value of 50
   incrementByTen()
   // returns a value of 60

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

Escaping Closures

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

※関数を抜けても引数を使いつずける場合はEscaping Closuresを使わないといけません。エラーとなります。

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

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

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

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

通常、クロージャは、クロージャの本体で変数を使用して暗黙的に変数をキャプチャしますが、この場合は明示的にする必要があります。自己をキャプチャする場合は、使用するときに明示的に自己を記述するか、クロージャのキャプチャリストに自己を含めます。自己を明示的に書くことで、意図を表現でき、参照サイクルがないことを確認するように促されます。たとえば、以下のコードでは、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)
   // Prints "200"
   completionHandlers.first?()
   print(instance.x)
   // Prints "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は変更可能です。 これは、エスケープするクロージャは構造体の自己への変更可能な参照をキャプチャできないという規則に違反します。

Autoclosures

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

関数が評価された後で実行されます。無駄な計算などを防ぐことができます。

autoclosureという呼称を使い機能を呼び出すことができます。たとえば、assert(condition:message:file:line :)関数は、その条件とメッセージパラメータのautoclosureを取ります。その条件パラメーターはデバッグビルドでのみ評価され、そのメッセージパラメーターは条件がfalseの場合にのみ評価されます。

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

   var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
   print(customersInLine.count)
   // Prints "5"
   let customerProvider = { customersInLine.remove(at: 0) }
   print(customersInLine.count)
   // Prints "5"
   print("Now serving \(customerProvider())!")
   // Prints "Now serving Chris!"
   print(customersInLine.count)
   // Prints "4"

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

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

    // customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
   func serve(customer customerProvider: () -> String) {
       print("Now serving \(customerProvider())!")
   }
   serve(customer: { customersInLine.remove(at: 0) } )
   // Prints "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を使いすぎると、コードが理解しにくくなる可能性があります。 コンテキストと関数名は、評価が延期されていることを明確にする必要があります。

関数が戻った後に呼び出すautoclosureが必要な場合は、@ autoclosure属性と@escaping属性の両方を使用します。 @escaping属性については、上記の「Escaping Closures」で説明しています。

    // 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!"

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


この記事が気に入ったらサポートをしてみませんか?