muapps

iOSアプリ開発で得られた知見をメモ代わりに投稿します。

Clousure - クロージャ

公式ドキュメントを読んでクロージャの整理。

docs.swift.org

クロージャはコード内で受け渡しできる自己完結型のブロック。
クロージャは定数および変数への参照をそれらが定義されているコンテキストからキャプチャして保存できる。

グローバル関数とネストされた関数はクロージャの特殊なケース。 クロージャの形式は以下の3つ。

クロージャ

外からクロージャを渡す場合

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

クロージャ式の構文の一般的な形式

{ (parameters) -> return type in
    statements
}

一般的なクロージャ式の構文そのままで書くと以下のように書ける。

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

この時Swiftは引数の型と戻り値の型を推測できるので以下のように書ける。

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

Swiftはインラインクロージャに短縮引数名を自動的に提供する。 inキーワードも省略できる。

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

関数の末尾のクロージャ(トレーリングクロージャ)

関数の最後の引数としてクロージャ式を関数に渡す必要があるとき末尾のクロージャとして記述できる。末尾のクロージャ構文を使用する場合引数ラベルは記述しない。

sortedメソッドを末尾のクロージャを使って書くと

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

クロージャ式が関数またはメソッドの唯一の引数で末尾のクロージャとして書く場合関数またはメソッドの名前の後に括弧のペアは記述しなくていい。

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

末尾のクロージャは、クロージャが十分に長く、1行にインラインで書き込むことができない場合に最も役に立つ。

関数が複数のクロージャを取る場合

関数を呼び出すとき最初の末尾のクロージャの引数ラベルを省略し、残りの末尾のクロージャにラベルを付ける。

定義

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

呼び出し

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

値をキャプチャする

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

ネストされた関数の場合

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

incrementer()はrunningTotalとamountを周囲からキャプチャする。

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

参照によるキャプチャはrunningTotalとamountがmakeIncrementerの呼び出しが終了した時消えないこととrunningTotalが次にincrementerが呼ばれたときに使用可能なことを保証する。

次のincrementByTenはincrementer関数を参照する。incrementer関数は呼び出される度にrunningTotalに10を足す。

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

2つ目のincrementerを作ると先述のものとは別の新しいrunningTotalへの参照を持つ。

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

クロージャエスケープする

関数の引数として渡されたクロージャが関数が返された後に呼ばれるときクロージャが関数をエスケープすると言う。エスケーピングクロージャがselfをキャプチャするときは明示的にselfを記載するかキャプチャリストに含める必要がある。selfを明示的に記載することで意図を表現でき参照サイクルがないことを確認するように促される。

// エスケーピングクロージャ
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

// 通常のクロージャ
func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 } // xを明示的にキャプチャする
        someFunctionWithNonescapingClosure { x = 200 } // xを暗黙的にキャプチャする
    }
}
キャプチャリストにselfを含める場合の書き方
// someFunctionWithEscapingClosure { [self] in x = 100 }

※オプショナル型はデフォルトで@escapingになる Optional Non-Escaping Closures – Ole Begemann

構造体・列挙体の場合

selfが構造体または列挙体のインスタンスである時はselfを常に暗黙的に参照できる。ただしこの時エスケーピングクロージャはselfへのmutableな参照をキャプチャできない。構造体と列挙体は共有された可変性を許可していない。

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

上記でsomeFunctionWithEscapingClosureはmutationgメソッドの中にありselfはmutable なのでエラーになる。

オートクロージャ

オートクロージャとは関数の引数として渡される式をラップするために自動的に作成されるクロージャのこと。引数をとらず、呼び出されるとその中にラップされている式の値を返す。

オートクロージャを使用すると引数として関数を取る時に{}を使ってクロージャとして書かずに通常の引数のように書ける。

@autoclosureを使わずに書く場合

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

@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属性の両方を使用する。