Variabel tidak hidup selamanya — ia hanya dapat diakses dalam wilayah kode tertentu yang disebut scope. Memahami scope membantu kita menulis kode yang rapi (variabel lokal hanya hidup saat dibutuhkan) dan menghindari bug akibat variabel yang tak sengaja bocor ke luar wilayahnya. Pada chapter ini kita akan membedah block scope { }, perilaku shadowing, loop variable scope, serta scope function run { } untuk membuat wilayah variabel eksplisit.

Block scope #

Di Kotlin, sebuah blok kode { ... } membentuk scope baru. Variabel yang dideklarasikan di dalam blok tidak dapat diakses di luar blok tersebut. Sebaliknya, variabel di luar blok tetap dapat diakses dari dalam blok.

kotlin Block scope
val pesan = "halo"   // scope luar
run {
    val pesanLokal = "dalam blok"   // scope dalam
    println(pesan)        // sah: akses scope luar
    println(pesanLokal)   // sah
}
// println(pesanLokal)   // galat: pesanLokal tidak terlihat di sini

Variabel lokal dalam blok hanya hidup selama blok tersebut dieksekusi. Setelah blok berakhir, referensi ke variabel tersebut tidak lagi valid dan memori dapat diklaim kembali oleh garbage collector.

Inner shadowing #

Kotlin memperbolehkan shadowing — mendeklarasikan variabel di scope dalam dengan nama yang sama dengan variabel di scope luar. Variabel luar disembunyikan (di-shadow) oleh variabel dalam selama blok dalam dieksekusi.

kotlin Shadowing
val nama = "Budi"
run {
    val nama = "Siti"      // shadow variabel luar
    println(nama)          // Siti
}
println(nama)              // Budi (kembali ke scope luar)

Meskipun sah secara sintaks, shadowing membuat kode sulit dibaca. Pembaca bisa keliru mengira variabel dalam adalah variabel luar.

Loop variable scope #

Variabel yang dideklarasikan dalam loop (misalnya for (i in 1..3) { val item = ... }) hanya hidup untuk satu iterasi. Pada iterasi berikutnya, variabel tersebut dianggap baru.

kotlin Loop variable scope
for (i in 1..3) {
    val item = "item-$i"   // baru setiap iterasi
    println(item)
}
// println(item)  // galat: item tidak terlihat

Variabel i dari loop juga hanya hidup dalam loop — tidak dapat diakses setelah for selesai. Perilaku ini berlaku juga untuk while dan do-while.

Scope function run { } #

Kotlin menyediakan beberapa scope function untuk membentuk blok dengan wilayah variabel eksplisit. Yang paling umum dipakai untuk membentuk blok adalah run { }. Fungsi run menjalankan blok lambda dan mengembalikan hasil ekspresi terakhirnya.

kotlin Scope function run
val total = run {
    val harga = 5000
    val qty = 4
    harga * qty          // nilai kembalian
}
println(total)  // 20000

Variabel harga dan qty hanya hidup dalam blok run. Setelah blok berakhir, keduanya tidak dapat diakses — wilayah variabel bersih dan terbatas.

Demonstrasi scope dan lifetime #

Berikut program lengkap yang memperlihatkan berbagai perilaku scope:

kotlin
val penggunaGlobal = "Budi"

fun main() {
    val pesan = "Halo dari main"
    println(pesan)

    run {
        val pesanBlok = "Halo dari blok dalam"
        println(pesanBlok)
        println("Akses pesan luar: $pesan")
    }

    println()
    val totalBelanja = 100_000
    run {
        val diskon = 10_000
        val totalSetelahDiskon = totalBelanja - diskon
        println("Total belanja       : $totalBelanja")
        println("Diskon              : $diskon")
        println("Total setelah diskon: $totalSetelahDiskon")
    }

    println()
    println("Loop variable scope:")
    for (i in 1..3) {
        val item = "item-$i"
        println("Memproses $item")
    }

    println()
    println("Shadowing (dapat dikompilasi tetapi tidak dianjurkan):")
    val namaLuar = "Budi"
    run {
        val namaLuar = "Siti"
        println("Di dalam run: namaLuar = $namaLuar")
    }
    println("Di luar run : namaLuar = $namaLuar")

    println()
    println("Lifetime setelah blok berakhir:")
    run {
        val temporary = listOf(1, 2, 3)
        println("Di dalam blok: temporary.size = ${temporary.size}")
    }

    println()
    println("Akses variabel global: penggunaGlobal = $penggunaGlobal")

    println()
    fun hitung(): Int {
        val sementara = 42
        return sementara * 2
    }
    println("Hasil hitung() = ${hitung()}")
}

Output yang Dihasilkan:

text
Halo dari main
Halo dari blok dalam
Akses pesan luar: Halo dari main

Total belanja       : 100000
Diskon              : 10000
Total setelah diskon: 90000

Loop variable scope:
Memproses item-1
Memproses item-2
Memproses item-3

Shadowing (dapat dikompilasi tetapi tidak dianjurkan):
Di dalam run: namaLuar = Siti
Di luar run : namaLuar = Budi

Lifetime setelah blok berakhir:
Di dalam blok: temporary.size = 3

Akses variabel global: penggunaGlobal = Budi

Hasil hitung() = 84

Mari kita amati baris-baris penting pada kode di atas:

  1. Baris 1 (val penggunaGlobal = "Budi") — variabel top-level; scope-nya mencakup seluruh file. Dapat diakses dari main dan function lain di file yang sama.
  2. Baris 4-5 (val pesan = "Halo dari main"; println(pesan)) — variabel lokal pesan hidup dalam main. Setelah main berakhir, pesan tidak lagi valid.
  3. Baris 7-11 (run { val pesanBlok = ...; println(pesanBlok); println(pesan) }) — blok run membentuk scope dalam. pesanBlok hanya hidup di blok ini. Variabel pesan dari scope luar tetap dapat diakses dari dalam.
  4. Baris 14-20 (val totalBelanja = 100_000; run { val diskon = ...; ... }) — totalBelanja di-deklarasi di scope main, dapat diakses dari dalam run. Sebaliknya, diskon dan totalSetelahDiskon hanya hidup dalam blok run.
  5. Baris 23-26 (for (i in 1..3) { val item = ... }) — variabel i dan item hanya hidup dalam satu iterasi. Setiap iterasi membuat instance baru.
  6. Baris 30-36 (val namaLuar = "Budi"; run { val namaLuar = "Siti" }) — shadowing: variabel dalam run menyembunyikan variabel luar dengan nama yang sama. Setelah run berakhir, variabel luar kembali terlihat. Kode dapat dikompilasi tetapi membingungkan — dianjurkan dihindari.
  7. Baris 40-43 (run { val temporary = listOf(1,2,3); println(...) }) — variabel temporary hanya hidup dalam blok. Setelah blok berakhir, referensi temporary keluar dari scope; memori yang dipakai dapat di-garbage collect.
  8. Baris 46 (println("Akses variabel global: penggunaGlobal = $penggunaGlobal")) — penggunaGlobal dapat diakses dari mana pun dalam file. Top-level scope tidak pernah berakhir selama program berjalan.
  9. Baris 49-52 (fun hitung(): Int { val sementara = 42; return sementara * 2 }) — variabel sementara hanya hidup selama pemanggilan function. Setiap pemanggilan hitung() membuat instance sementara baru.

Tabel ringkas scope #

WilayahScope variabel
Top-level (val x di file)Seluruh file, sepanjang program berjalan
Dalam functionSelama pemanggilan function
Dalam blok { } atau run { }Selama blok dieksekusi
Dalam loop for/whileSatu iterasi
Dalam lambda parameterSelama pemanggilan lambda
Dalam try/catchSelama blok dieksekusi

Scope function lainnya #

Selain run, Kotlin menyediakan beberapa scope function dengan kombinasi cara akses dan nilai kembalian berbeda:

FunctionReceiverReturnKegunaan
run { }tidak ada (lambda)Hasil lambdaBlok dengan scope variabel lokal
let { }itHasil lambdaTransformasi nullable
apply { }thisReceiverKonfigurasi object
also { }itReceiverSide effect tanpa mengganggu chain
with(x) { }this (dari arg)Hasil lambdaOperasi beruntun pada object

Pembahasan lengkap scope function tersedia pada chapter khusus di section lanjutan.

Referensi Lanjutan #

  • Properties — dokumentasi properti Kotlin yang menjelaskan scope val/var dan backing field
  • Coding conventions — panduan konvensi Kotlin untuk memilih scope yang tepat bagi variabel lokal dan properti

Pemahaman scope dan lifetime membantu kita menulis kode yang rapi dan minim bug. Berikutnya, kita akan menutup section ini dengan konvensi penamaan variabel — pada chapter Naming Conventions.