swift言語:closure

概要

The Swift Programming Language (Swift 2.1): ClosuresでClosureについて調べました。

以下は、 The Swift Programming Language (Swift 2.1): ClosuresのClosureに関する概要を超訳した内容です。

Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages.

Closureは関数の自分自身を含む処理ブロックであり、使い回すこともできるし、コードの中で使うこともできます。SwiftのClosureは、CやObjective-Cのブロックあるいは他のプログラミング言語のラムダと似ています。

注)self-contained blocks of functionalityの意味がよく分かっていません。

Closures can capture and store references to any constants and variables from the context in which they are defined. This is known as closing over those constants and variables, hence the name “closures”. Swift handles all of the memory management of capturing for you.

Closureは、Closureが定義されているコンテキストから参照できる定数や変数をキャプチャし保存することができます。これは、定数や変数の閉じ込め(閉包)として知られています。したがって、“closures”という名前が付いています。Swiftは、Capturingに関する全てのメモリ管理を取り扱ってくれます。

Global and nested functions, as introduced in Functions, are actually special cases of closures. Closures take one of three forms:
  • Global functions are closures that have a name and do not capture any values.
  • Nested functions are closures that have a name and can capture values from their enclosing function.
  • Closure expressions are unnamed closures written in a lightweight syntax that can capture values from their surrounding context.

グローバル関数やネスト関数は、Functionの章で紹介したように、実際にはclosureの特殊なケースです。Closureは以下の3つのどれかになります。

  • グローバル関数:名前を持ち、どんな値もキャプチャしないclosure。
  • ネスト関数:名前を持ち、包含した関数から値をキャプチャするclosure。
  • その他:取り囲んでいるコンテキストから値をキャプチャする、簡易なシンタックスで書かれた名前なしclosure。

Swift’s closure expressions have a clean, clear style, with optimizations that encourage brief, clutter-free syntax in common scenarios. These optimizations include:
  • Inferring parameter and return value types from context
  • Implicit returns from single-expression closures
  • Shorthand argument names
  • Trailing closure syntax

Swiftのclosure表現は、一般的な利用シナリオにおいて短く、整理された構文になるよう最適化が行われています。 以下の最適化が行われています。

  • コンテキストからパラメータや返値の型推論
  • 単一処理closureの場合返値型省略
  • 短縮引き数名
  • 後置closure構文

Sortメソッド

文字列配列のsortメソッドを使ってClosureの説明を行います。

sortメソッド

文字列配列のsortメソッドは、同じ型を持つ二つの引数を持つ関数(Closure)を引数にとります。 以下の例は、辞書降順ソートするbackwardsを引数にしてソートを実行しています。

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

func backwards(s1: String, _ s2: String) -> Bool {
    return s1 > s2
}

let reversed = names.sort(backwards)
// ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

コンテキストからパラメータや返値の型推論

型推論によってどのように簡潔に記述できるか説明します。

Closureの書式

{ (パラメータ) -> 返値型 in
	処理ブロック
}

Closureを使うとsort関数は、以下のように書くことができます。これは、他のプログラミング言語で、無名関数と呼ばれます。

reversed = names.sort({ (s1: String, s2: String) -> Bool in
    return s1 > s2
})

ブロックが短い場合には、1行に書くことも出来ます。

names.sort( { (s1: String, s2: String) -> Bool in return s1 > s2 } )

型推論による短縮表記

文字列配列namesのソート(要素の比較)なので、引数s1,s2は文字列になります。したがって型Stringを省略することができます。

names.sort( { s1, s2 in return s1 > s2 } )

単一処理closureの場合返値型省略

処理ブロックが1行の場合にはreturn文を省略できます。

names.sort( { s1, s2 in s1 > s2 } )

短縮引数名

names.sort( { $0 > $1 } )

Operator関数

文字列型の比較演算子(例:>)が二つの文字列が入力で返値がBoolとして定義されています。したがって、文字列ソートの場合は、比較演算子を使うことができます。

names.sort(>)

Trailing Closures

names.sort( { $0 > $1 } )

Trailing ClosureとしてClosureを外に出すことができます。

names.sort() { $0 > $1 }

クロージャだけが引数の場合にはかっこを省略することができます。

names.sort { $0 > $1 }

処理ブロックが長い場合に、有効です。()が付けた場合、対応関係が見づらくなります。

数値配列に入っている数値を文字に変換する処理例です。

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]

let strings = numbers.map {
    (var number) -> String in
    var output = ""
    while number > 0 {
        output = digitNames[number % 10]! + output
        number /= 10
    }
    return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

Capturing Values

以下の関数は、forIncrementという外部引数名を持ち、引数なしでInt型の値を返す関数を返す関数です。返値として返した関数は、関数の外にある変数runningTotalを参照することができます。

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
// 10づつ値を増やす関数を取得します
let incrementByTen = makeIncrementer(forIncrement: 10)
// 関数を呼ぶ度に返す値が増えていきます。
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

// インスタンスごとに変数を持ちます。
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

Closures Are Reference Types

Closuresはリファレンス型なので、別変数にセットされてもコピーされません。

// incrementByTenが参照しているインスタンスのrunningTotalが40だとすると
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

Nonescaping Closures

関数を引数に渡すとき、@noescapeというキーワードをパラメータに付けると、関数実行時に処理が行われます。デフォルトは@escapeで引数の関数が呼び出されるまで処理が遅延されます。

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: () -> Void) {
    completionHandlers.append(completionHandler)
}
func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) {
    closure()
}
class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 } <-- (A)
        someFunctionWithNoescapeClosure { x = 200 } <-- (B)
    }
}
 
let instance = SomeClass()
// doSomethingメソッド実行時に
// someFunctionWithEscapingClosureに渡されたClosureは実行されない(A)。
// someFunctionWithNoescapeClosureに渡されたClosure(x = 200)は実行される(B)。
instance.doSomething()
print(instance.x)
// prints "200"

// 明示的にメソッド呼び出しして初めて処理が実行される。
// self.x = 100
completionHandlers.first?()
print(instance.x)
// prints "100"

Autoclosures

Autoclosuresは、引数を関数として渡す処理を行ってくれる自動的にキーワードです。

autoclosureは、実際に関数実行するまで引数(関数)処理を遅延してくれます。

auto closureを付けない場合

func serveCustomer(customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serveCustomer( { customersInLine.removeAtIndex(0) } )

auto closureを付けた場合

func serveCustomer(@autoclosure customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serveCustomer(customersInLine.removeAtIndex(0))

関連資料