[Kotlin] クロージャを利用してカウンターを作る

クロージャと聞くと頭が痛くなる人も多いのではないでしょうか。

理解するのが難しいクロージャ、今回は細かいことはともかく、使えるようになろうということで、ザックリと通常の関数との違い、クロージャのメリットや使い方をお伝えしようと思います。

クロージャを定義して呼び出す

通常の動作

例としてシンプルな高階関数を定義してみます。

fun increment(): ()->Int{
    var x = 0
    return {
        x++
        x
    }
}

increment関数自体は引数を取らず、ラムダ式で記述された無名関数をreturnする高階関数です。

ラムダ式は引数を取らず、Int型を返します。

内部では1つの変数を定義し、それをラムダ式でインクリメントして出力しています。

この関数を呼び出してみましょう。

fun main(){
    val close = increment()
    println(close())
}
//1

始めに名前付き関数incrementの呼び出しを変数に代入しています。

こうすることで、incrementから返されるラムダ式を、increment関数から独立した(ように見える)1つの関数として扱えます。

このcloseの中身はラムダ式です。それを呼び出して標準出力すると、increment関数で定義されたxを読み込み、xを+1した結果の1が出力されます。

ここまでは普通の動作です。

関数のスコープ内で定義された関数(ラムダ式)は、同じスコープで定義された変数にアクセスできるというのは、前回お話したとおりです。

スコープの基本については以下をご覧ください。

[Kotlin] スコープの基本 - 制限によって安全を確保する
アクセスを制限することが安全につながるということは往々にしてあります。 プログラ...

クロージャとしての動作

ではこの関数呼び出しを2回連続で行ってみましょう。

fun main(){
    val close = increment()
    println(close())
    println(close())
}
//1
//2

関数の呼び出しごとに、値が増えています。これは通常の関数ではあり得ない挙動です。

通常の関数は、関数内で定義された変数を関数終了時点で破棄します。これが通常の挙動であり原則です。

ただ、関数を返す関数を関数オブジェクトとして使用した場合、外側の関数のスコープは変数closeによって保持され、無名関数は外側の関数内で定義された変数を、プログラム終了まで参照し、保持し続けることができます。

なぜ保持できるのかについては、アクティベーションレコードコールスタックの理解が必要になるので、こちらではそこまで踏み込むことはしません。興味の有る方は調べてみてください。

クロージャのメリット

クロージャを使うメリットとして、グローバルなスコープ(Kotlinで言えばファイルレベルやmain関数のスコープ)を使うこと無く、無名関数からしかアクセスできない、安全性の高い変数を使えるという点があります。

上記と同じ機能をクロージャを使わずに記述しようとすると、どうしてもmain関数などの、より外側のスコープを使わざるを得なくなります。

fun main(){
    var x = 1
    val increment = {x++}
    println(increment())
    println(increment())
}
//1
//2

このコードだとmain関数内で使用している変数は2つになります。このどちらも削ることができません。

さらに値が保存されているのは変数xであり、この変数はmain関数内であればどこからでもアクセスできてしまいます

プログラムの基本としてグローバルスコープや、main関数といった実行時に走るメソッド内での変数定義はできるだけ減らすべきだという考え方があります。

今までお話したような意図しない変更から守るため、あるいはもっと単純な「名前の衝突」の問題です。

クロージャを使用することで、安全でしかも名前空間を極力汚さないプログラムにできるというのは大きなメリットです。

複数のクロージャを生成する

クロージャを利用すると複数のカウンターを生成することも簡単にできます。

最初の関数定義に少々手を加えてみましょう。

fun increment(num: Int): ()->Int{
    var x = num
    return {
        x++
        x
    }
}

変更点はincrement関数のパラメータと変数xです。ここでは引数にInt型を取り、それをxに代入しています。

fun main(){
    val close = increment(0)
    val close2 = increment(10)

    println(close())
    println(close())
    println(close2())
    println(close2())
}
//1
//2
//11
//12

関数オブジェクトを変数に代入する際、その変数名を複数設定するだけで、それぞれの呼び出しは全く別の関数を呼び出しているかのように振る舞います。

ちなみにここでは、2つの変数に代入しているincrement関数の呼び出しに別の数値を使用していますが、同じでもかまいません。

クロージャ利用時の注意点 – 値を保存できない場合

使い方によっては便利なクロージャですが、以下のような呼び出しでは、値を保持する関数として動作させることができません。

println(increment()())
println(increment()())
//1
//1

この場合はincrement関数のスコープが1行ごとに失われてしまい、それによって無名関数も値を保持することができず、値が増えていくことはありません。

クロージャとして動作させるのであれば、まず関数オブジェクトとして無名関数を独立させることが必要になるので注意しましょう。

おさらい

今回はクロージャをザックリと解説してみました。クロージャの定義としては、

  • 関数内に定義された関数(多くはラムダ式や無名関数)である
  • そのラムダ式や無名関数は、外側の関数で定義された変数を参照している
  • 関数オブジェクトに代入された内側の関数は、外側の関数で定義された変数を、プログラム終了まで保持し続けられる
  • よって名前空間を汚さないカウンターや、一度定義された変数を呼び出しごとに定義し直すような、無駄なプロセスを省くことができる

実際これがクロージャの全てではありませんが、初心者の方であればこんな感じの理解ができていれば、最初のうちは困ることは無いのではないでしょうか。

長々と関数について書いてきましたが、次のトピックで関数については一旦終了です。次回はKotlinの標準関数について。

タイトルとURLをコピーしました