[Kotlin] スコープの基本 – 制限によって安全を確保する

アクセスを制限することが安全につながるということは往々にしてあります。

プログラミングで言えば、その制限の1つがスコープです。今回はスコープについて、Kotlinを題材にしてできるだけ分かりやすくお話しようと思います。

スコープとは?

まずはこのスコープが何であるのかを理解していきましょう。

一言で言ってしまうとスコープとは、ある変数を読み書きできる範囲のことです。

スコープはその変数がどこで定義されたかによって決まります。最初はこのようなコードを例にとって見ていきましょう。

ファイルレベル(トップレベル)

var x = 10
fun main(){
    println(x)
}
//10

このコードではmain関数の外で変数を定義し、それをmain関数で使用しています。

このように、どこの関数にも属していない変数をファイルレベルの変数、あるいはトップレベルの変数と呼びます。

PythonやJavaScriptなどで言えばグローバルスコープの変数です。

この変数は、このファイル内のどこからでもアクセスできる変数です。

ただ、どこからでもアクセスできるというのは自由で便利な半面、大きなデメリットもあります。

ファイルレベル変数の怖さ

どこからでもアクセスできるという状況は、逆に言えば「もし意図しない変更が見つかった場合、そのファイルのどこで意図しない値が混入したか分からない」ということにもなり得ます。

以下のコードを見てみましょう。

var x = 10
fun main(){
    println(x)
    count()
    println(x)
}

fun count(){
    x+=1
}
//10
//11

コードの下側に定義されたcount関数ではファイルレベル変数の値を変更しています。

このcount関数を呼び出した影響で、同じprintln(x)の結果が、count関数を呼び出す前と後で違うという現象が起きます。

この場合は関数が2つしか無いために大した問題にはなりませんが、もし他の関数が100個あったとしたらどうでしょう?

これがファイルレベル変数の怖さです。このためファイルレベルで変数を定義するのは、値が変化しないファイルレベル定数や、その他限定された状況に限られます。

ローカルスコープ

ファイルレベル変数のようにどこからでもアクセスできるオープンな変数というのは、その危険性のために実際の開発ではあまり使えません。

それではこの変数xを、意図しない変更から守るには?

fun main(){
    var x = 10
    println(x)
    count()
    println(x)
}

fun count(){
    x+=1          //エラー
}
//エラー:(9, 5) Kotlin: Unresolved reference: x

xという変数は(count関数から見える範囲では)見当たらない

変数xの定義はmain関数内に収まり、これによって他の関数からの参照や変更を受け付けなくなりました。

このように関数内で定義された変数をローカル変数(ローカルスコープの変数)といいます。

ローカル変数は、定義されたスコープ内(ここではmain関数内)でしか参照、変更できません。

関数内関数

では関数の中に関数が定義されるような場合、そのスコープはどうなるのか。以下がその例です。

fun main(){
    fun getNumber(){
        val x = 10
        println(x)
        fun getMultiple(): Int{
            val y = x*5
            return y
        }
        println(getMultiple())
    }
    getNumber()
}

main関数の中には引数、戻り値無しのgetNumber関数が定義されています。

getNumber関数はその内部で変数を定義し、それをそのまま標準出力します。

その下は入れ子になった関数定義です。getMultiple関数の内部ではgetNumber関数で定義された変数xにアクセスし、その値を5倍にして返しています。

これは普通に動作するコードです。ただし、getNumber関数からgetMultiple関数の変数yにはアクセス出来ません。

つまりファイルレベル、関数のローカルスコープ、その中のローカルスコープがあるとすると、このような関係になっていることが分かります。

これは関数だけではなく、制御構文のブロックでも同じように働きます。

もう1つローカル変数、ブロックスコープをご紹介しましょう。

ブロックスコープ

fun main(){
    var x = 10
    println(x)
    for(i in 1..3){
        var x = 100
        print("${i}回目")
        println(x)
    }
    println(x)
}
//10
//1回目100
//2回目100
//3回目100
//10

main関数内にfor文を記述して、同じ処理を3回繰り返しています。この中で使用されているiとxはfor文のローカル変数です。

iのように制御構文のブロック内({}で囲まれた部分)だけで使われる変数や、ブロック内で定義されるxのような変数は、関数のときと同じように外側から参照できない変数になります。

この変数をブロックスコープの変数と呼びます。

ちなみにvarキーワードが無かった場合はmain関数のxを参照して変更するので、ブロック内であってもできるだけ変数の名前は固有であることが、安全のためにはベターです。

逆に制御構文の最終的な結果を他の場所で使いたい場合、制御構文内部でvarを使わないことによって、この場合であればmain関数内の他の場所でも、制御構文を通った後の変数xを使用することができます。

ローカル変数の寿命

forの中でxはmain関数とは別のxとして使用され、main関数にまで影響を与えることはありません。

コードの最後で記述したprintln(x)の結果は10です。これはmain関数で定義したxであって、for文の中で定義したものではありません。

上の図で示したように、関数や制御構文で使われるローカル変数は、そのブロックが終了すると同時に破棄され、無かったことになります。これがローカル変数の寿命です。

関数の入出力

スコープという観点から言えば、関数を定義するというのは、イメージで言うと枠を設定しているようなものです。

枠を設定することによって、外側からの変更というリスクから内部の変数を守り、外部に変数を出さないことによって関数内での自由を確保します。

ただし全く閉じられた枠ではありません。関数の入力は引数、出力はreturnに制限し、安全性と自由度のバランスを保ちながら、プログラムの他の部分とやり取りをすることが許されています。

おさらい

これまでのことをまとめると、

  1. ファイルレベルは全ての関数の外側に位置し、どこからでもアクセス可能
  2. 関数内で定義されたローカル変数は、その外側のスコープからアクセスできない
  3. 別々の関数は、お互いのローカル変数にアクセスできない
  4. 関数や制御構文が入れ子になっている場合、外側から内側のローカル変数へはアクセスできない
  5. しかし内側から外側のローカル変数へのアクセスは可能
  6. ローカル変数は、定義されたスコープが終了すると破棄される

次回は難しいテーマ、クロージャについて。

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