Swiftのクロージャ、値のキャプチャ(Capturing Values)

クロージャ

ここでは、クロージャ(関数)における、値のキャプチャ(Capturing Values)について説明します。入れ子にした関数(nested function)が一番分かりやすいので、その具体例を使って値のキャプチャを詳しく説明していきます。また、実はクロージャは(クラスと同様に)参照型である、ということについて簡単に触れます。

値のキャプチャ(Capturing Values)とは?

Captureは「読み取る」とか「取り込む」、または「とらえる」といった訳が当てられています。値のキャプチャ(Capturing Values)とは、すでにスコープを外れた定数や変数の値を参照したり変更したりすることです。日本語だと分かりにくいですが、実は、入れ子にした関数(nested functions)は、値のキャプチャが可能な最も簡単な例です。したがって、ここでは単純な入れ子関数の実例を使って説明していきます。

Nested functionにおける値のキャプチャ | スコープを外れた値の参照

では、nested functionを使って値のキャプチャの説明をします。

// 値のキャプチャ | 総乗を計算する関数
func makeProduct(number: Double) -> () -> Double {
    var result = 1.0
    func product() -> Double {
        result *= number
        return result
    }
    return product
}

公式マニュアルにあるmakeIncrementer()と構造は一緒ですが、演算部分が和ではなく積になっています。

関数makeProduct(number:)は、別の関数product()を入れ子にしています。関数の戻り値はfunction type() -> Doubleです。

// 戻り値は() -> Double
func makeProduct(number: Double) -> () -> Double {
    ....
}

これは、入れ子にした関数product()を戻り値とするための仕様です。product()は、パラメータであるnumberresultに掛け合わせています。掛け算なので、resultの初期値は1.0になっています。

このproduct()は、numberresultを外側の関数makeProduct(number:)からキャプチャしています。キャプチャというのが良く分かるように、入れ子にした関数だけを抜き出して見てみます。

// 入れ子にした関数
func product() -> Double {
    result *= number
    return result
}

product()はパラメータを持っていません。しかし、numberresultを参照しています。これがまさに値のキャプチャで、product()numberresultの参照(reference)を外側から持ってきて、product()本体で使用しています。

独立した関数呼び出しは、別々の参照元を持つ

Nested functionは値を参照しているので、makeProduct(number:)関数を使用した後でも、その参照が外れることはありません。これは実例で見た方が分かりやすいですので、実際にmakeProduct(number:)を使ってみます。

// 2を掛ける関数
let productOfTwo = makeProduct(number: 2.0);
print(productOfTwo()) // 2.0
print(productOfTwo()) // 4.0
print(productOfTwo()) // 8.0

この例では、productOfTwoという定数をmakeProduct(number: 2.0)で初期化しています。言い換えると、productOfTwomakeProduct(number: 2.0)を参照しています。この関数は、呼び出される度に、result2.0を掛け合わせるので、結果は上記のように、3回目の呼び出しでは2.0*2.0*2.0=8.0という結果になります。

もし、別の数字で総乗を計算する関数を作成すると、どうなるでしょうか?

// 3を掛ける関数
let productOfThree = makeProduct(number: 3.0);
print(productOfThree()) // 3.0
print(productOfThree()) // 9.0

ここでは、makeProduct(number: 3.0)を参照するproductOfThreeを作りました。今、productOfThreeは、新しい参照元を持っているので、演算結果はproductTwoとは独立した結果になっています。

Nested functionsを使った場合の値のキャプチャ

図で示すと上のようになります(Swift 2の時に作った絵なので、関数呼び出しのラベル指定がありません)。先程説明したように、別の関数コールで作成した定数・変数は、独立した参照元を持っています。したがって、別々の関数呼び出しで演算した結果が、どんどん積算されていくということはありません。

公式マニュアルのCapturing Valuesのページに注意書きがありますが、値のキャプチャでは、常に値を参照するわけではないということを留意下さい。

“NOTE

As an optimization, Swift may instead capture and store a copy of a value if that value is not mutated by a closure, and if the value is not mutated after the closure is created.

Swift also handles all memory management involved in disposing of variables when they are no longer needed.”

抜粋:: Apple Inc. “The Swift Programming Language”。 iBooks https://itun.es/jp/jEUH0.l

クロージャは参照型

Nested functionを使った値のキャプチャでは、呼び出した関数を定数に放り込んでいました。ところが、定数productOfThreeなどが参照している関数は、resultに値を掛け続けることが可能です。「定数だから、値を変更するのは無理じゃないのか?」と疑問に思いますが、実は、

クロージャ(つまり、関数も)は参照型

です。

参照型に関しては、クラスと構造体のページで詳しく説明しました。クロージャについても全く同様です。ある定数や変数にクロージャ(関数)を渡した場合、その定数などはクロージャ(関数)の参照を指しています。したがって、今定数になっているのは、クロージャ本体ではなく、その参照です。

また、参照型ですから、同じクロージャ(関数)を複数の定数や変数に渡した場合、それらの定数や変数は同じクロージャ(関数)を参照することになります。

// 同じ参照元を持つ定数
let anotherProductOfTwo = productOfTwo
print(anotherProductOfTwo()) // 16.0

anotherProductOfTwoは、productOfTwoと同じ参照を持っていますから、関数を呼び出すと、これまでの結果8.02.0が掛かった結果16.0を返すことが分かります。

まとめ

  • クロージャは、値をキャプチャ(スコープ外の値を参照)できる。
  • クロージャは(クラスと同じで)参照型。