なぜこの記事を書こうと思ったか
Swiftの勉強をしているときに「クロージャ」という単語が出てきてググっていたのですが,理解するまでに結構時間がかかってしまいました.クロージャを理解しにくい要因は,言葉の定義と具体的な実装の間をつなぐ説明が足りていないところにあると思ったので,この記事ではその間を埋めることを試みたいと思います.
具体例
まず一番わかりやすかったクロージャの例を出します.
// func 関数名(外部引数名 引数: 型) -> 返り値の型 {
// 処理内容
// }
func makeIncrementer(amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
let incrementByOne = makeIncrementer(amount: 1)
incrementByOne() // 1
incrementByOne() // 2
makeIncrementer()
は「Int型を返す関数を返す関数」です.そして,その「Int型を返す関数」は incrementer()
で makeIncrementer()
の内部で定義されています.incrementer()
は自身が定義されているスコープにある変数 runningTotal
を参照してそれに amount
を加えるという処理を行います.
最後に,実際にmakeIncrementer()
を引数 amount
を1にして呼び出して,帰ってきた関数を定数 incrementByOne
に代入します.incrementByOne
を2度呼び出してみます.すると一度目の実行結果は1
で二度目の実行結果は2
が返ってきます.
なぜインクリメントできるのか
どうして一度目と二度目の実行結果が両方とも 1
にはならないのでしょうか.言い換えると,二回目に呼ばれたときに前回の値がどうして保存されているのでしょう.これは,そもそもmakeIncrementer
の内部にあるincrementer
がクロージャとして定義されるからです.
クロージャとはなにか
クロージャとは関数が自身の定義されているコンテクストに含まれる変数や定数への参照を取り込んで保持しておく機能です.先程の例では,incrementer()
の定義に含まれているrunningTotal
への参照が保持されています.
クロージャはwikipediaでは次のように説明されています
クロージャ(クロージャー、英語: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。いくつかの言語ではラムダ式や無名関数にて利用可能な機能・概念である。引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる
この値を保持しておくこと自体は「キャプチャ(capture)」するといったりします.
なぜクロージャが理解しにくいか
以上の説明でクロージャの概念は大体掴めたかと思います.ここからはクロージャを理解しにくくなっている原因について,僕の意見を書きたいと思います.
クロージャ式だけがクロージャではない
Swiftにはクロージャ式(Closure Expression)というものがありクロージャを簡潔に記述できます.他言語でもラムダ式や無名関数を使ったことがあればその便利さがわかるかと思います.Swiftのクロージャ式では関数名を省略できたり,returnを省略したりできます.
// {(引数名1: 型, 引数名2: 型...) -> 返り値の型 in
// 処理内容
// }
let incrementByOne = { (x: Int) -> Int in
x + 1
}
incrementByOne(2) // 3
上のようなクロージャ式のことを指して,「これがクロージャです」と説明されていることが稀にあります.下で説明するようにswiftで関数を書いた場合はクロージャの要件を満たしていますから,その説明は間違っていません.間違ってはいませんがミスリードをしていると思います.クロージャとクロージャ式は別概念です.関数で書いてもクロージャ式で書いてもクロージャはクロージャです.
キャプチャする値がなくてもクロージャ
クロージャは言い換えると (1) 関数と (2) 関数の定義に必要だった変数や定数(への参照)の2つの組み合わせです.しかしながら,ややこしいことに,(2) がない場合もクロージャの特別な場合として説明されます.例えば以下のようなものがクロージャの例としてswiftの公式ページで説明されています
- グローバル関数は、名前を持ち、値をキャプチャしないクロージャです。
- ネストされた関数は、名前を持ち、包含する関数から値をキャプチャできるクロージャです。
- クロージャ式は、周囲のコンテキストから値をキャプチャできる軽量な構文で記述された名前なしクロージャです。
2つめのネストされた関数は最初に出した例です.値をキャプチャできると言っているだけで,値をキャプチャしていなくてもクロージャの一種であることに注意してください.
まとめ
「クロージャ」とは関数が自身の定義されているコンテクストに含まれる変数や定数をキャプチャする機能です.しかしクロージャ式で書かれた値をキャプチャしていない関数を例として挙げて説明されることがあります.この定義と具体例の乖離がすべての混乱の原因になっていると思います.
僕も100%理解したわけではないですが,ググって得た情報を組み合わせて,わかったことを書き起こして置きました.なにか不明な点があればコメント等でご指摘いただけると助かります.
参考
What is a 'Closure'? - stackoverflow
https://docs.swift.org/swift-book/LanguageGuide/Closures.html
https://docs.swift.org/swift-book/LanguageGuide/Closures.html#ID103
なぜクロージャ(Closure)と言うのか?
Swiftでクロージャを理解する
クロージャまとめ(Swift )