Swiftでプログラミング。 - Closures 2
Trailing Closures
クロージャ式を関数に最後の引数として渡す必要があり、クロージャ式が長い場合は、代わりにtrailing closure として記述すると便利です。 関数呼び出しの括弧の後にtrailing closure を記述します。 trailing closure 構文を使用する場合、関数呼び出しの一部として最初のクロージャの引数ラベルを記述しません。 関数呼び出しには、複数のtrailing closure を含めることができます。 ただし、以下の最初のいくつかの例では、単一のtrailing closure を使用しています。
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
// Here's how you call this function without using a trailing closure:
someFunctionThatTakesAClosure(closure: {
// closure's body goes here
})
// Here's how you call this function with a trailing closure instead:
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
上記の文字列ソートクロージャは、sorted(by :)メソッドの括弧の外側に末尾のクロージャとして記述できます。
reversedNames = names.sorted() { $0 > $1 }
クロージャ式が関数またはメソッドの唯一の引数として提供され、その式を末尾のクロージャとして提供する場合、関数を呼び出すときに、関数またはメソッドの名前の後に括弧()のペアを記述する必要はありません。
reversedNames = names.sorted { $0 > $1 }
trailing closureは、クロージャが長い場合、1行で簡潔に書き込むことができない場合に最も役立ちます。 例として、Swiftの配列型にはmap(_ :)メソッドがあり、単一の引数としてクロージャ式を取ります。 クロージャは、配列内のアイテムごとに1回呼び出され、そのアイテムの代替のマップされた値(おそらく他のタイプの値)を返します。 map(_ :)に渡すクロージャーにコードを記述することにより、マッピングの性質と戻り値のタイプを指定します。
提供されたクロージャを各配列要素に適用した後、map(_ :)メソッドは、元の配列の対応する値と同じ順序で、すべての新しいマップされた値を含む新しい配列を返します。
trailing closureで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(_ :)メソッドにtrailing closureとして渡すことにより、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
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]
map(_ :)メソッドは、配列内のアイテムごとにクロージャ式を1回呼び出します。クロージャーの入力パラメーターであるnumberの型を指定する必要はありません。これは、型が移動される配列の値から推測できるためです。
この例では、クロージャ本体内で値を変更するため変数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(_ :)メソッドによって出力配列に追加されます。
上記の例でtrailing closure構文を使用すると、クロージャがサポートする関数の直後にクロージャの機能がきちんとカプセル化されます。クロージャ全体を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つのクロージャーを使用する代わりに、ネットワーク障害の処理を担当するコードを、ダウンロードが成功した後にユーザーインターフェイスを更新するコードから明確に分離できます。
Capturing Values
クロージャーは、それが定義されている周囲の文脈から定数と変数を取り込むことができます。 クロージャーは、定数と変数を定義した元のスコープが存在しなくなった場合でも、本体内からそれらの定数と変数の値を参照および変更できます。
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と呼ばれる整数変数を定義して、返されるincrementer()の現在の現在の合計を格納します。この変数は値0で初期化されます。
makeIncrementer(forIncrement :)関数には、forIncrementの引数ラベルとamountのパラメーター名を持つ単一のIntパラメーターがあります。このパラメーターに渡される引数値は、返されたincrementer()関数が呼び出されるたびに、runningTotalをインクリメントする量を指定します。 makeIncrementer関数は、実際のインクリメントを実行するincrementer()と呼ばれるネストされた関数を定義します。この関数は単にrunningTotalに金額を加算し、結果を返します。
単独で検討すると、ネストされたincrementer()関数は異常に見える場合があります。
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
インクリメント()関数にはパラメータがありませんが、関数本体内からrunningTotalとamountを参照します。 これは、runningTotalとamountへの参照を周囲の関数からキャプチャし、それらを独自の関数本体内で使用することによって行われます。 参照によるキャプチャにより、makeIncrementerの呼び出しが終了したときにrunningTotalとamountが消えないようにし、次にインクリメント関数が呼び出されたときにrunningTotalが使用可能になるようにします。
最適化として、Swiftは、値がクロージャによって変更されていない場合、およびクロージャの作成後に値が変更されていない場合、代わりに値のコピーをキャプチャして保存する場合があります。
makeIncrementer()の動作例を次に示します。
let incrementByTen = makeIncrementer(forIncrement: 10)
この例では、incrementByTenという定数を設定して、呼び出されるたびにrunningTotal変数に10を追加するインクリメンター関数を参照します。 関数を複数回呼び出すと、この動作が実際に動作していることがわかります。
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
2番目のインクリメントを作成すると、新しい別個のrunningTotal変数として保存されます。
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
最初のincrementer(incrementByTen)を再度呼び出すと、それ自体のrunningTotal変数が引き続きインクリメントされ、incrementBySevenによってキャプチャされた変数は影響しません。
incrementByTen()
// returns a value of 40
クラスインスタンスのプロパティにクロージャを割り当て、クロージャがインスタンスまたはそのメンバーを参照してそのインスタンスをキャプチャする場合、クロージャとインスタンスの間に強力な参照サイクルが作成されます。 Swiftは、キャプチャリストを使用して、これらの強力な参照サイクルを中断します。
この記事が気に入ったらサポートをしてみませんか?