Variabel

Variabel #

Ruby adalah bahasa yang dynamically typed — kamu tidak perlu mendeklarasikan tipe data sebelum menggunakan variabel. Tapi Ruby bukan berarti tidak punya aturan tentang variabel. Justru sebaliknya: Ruby sangat opinionated tentang di mana sebuah variabel bisa diakses. Sistem scope variabel di Ruby ditandai secara visual melalui prefix nama variabel itu sendiri — sehingga hanya dengan melihat nama variabel, kamu sudah bisa tahu ruang lingkup aksesnya. Memahami sistem ini dengan baik adalah fondasi untuk menulis kode Ruby yang benar, terutama ketika mulai bekerja dengan kelas dan objek.

Sistem Scope dan Prefix Variabel #

Di Ruby, jenis variabel ditentukan oleh karakter pertama dari namanya. Ini bukan sekadar konvensi — interpreter Ruby membaca prefix ini untuk menentukan bagaimana variabel tersebut disimpan dan dari mana ia bisa diakses.

PrefixJenisContohScope
Huruf kecil / _Variabel lokalnama, _tempDalam method atau blok saat ini
@Variabel instance@namaDalam seluruh instance objek
@@Variabel kelas@@jumlahDalam kelas dan semua instance-nya
$Variabel global$modeDi seluruh program
Huruf kapitalKonstantaMAX, PIDalam kelas atau modul (dibahas di artikel Konstanta)
flowchart TD
    A[Nama variabel dimulai dengan apa?] --> B["Huruf kecil / _"]
    A --> C["@"]
    A --> D["@@"]
    A --> E["$"]
    A --> F["Huruf KAPITAL"]
    B --> B1["Variabel Lokal\nHanya dalam method/blok ini"]
    C --> C1["Variabel Instance\nSeluruh objek, per-instance"]
    D --> D1["Variabel Kelas\nDibagi semua instance kelas"]
    E --> E1["Variabel Global\nDi mana saja dalam program"]
    F --> F1["Konstanta\nDibahas di artikel berikutnya"]

Variabel Lokal #

Variabel lokal adalah jenis variabel yang paling sering kamu gunakan sehari-hari. Ia hanya bisa diakses dalam method, blok, atau proc tempat ia didefinisikan — tidak lebih, tidak kurang.

def hitung_cicilan(harga, dp_persen, tenor_bulan)
  dp          = harga * dp_persen          # variabel lokal
  sisa_bayar  = harga - dp                 # variabel lokal
  cicilan     = sisa_bayar / tenor_bulan   # variabel lokal

  cicilan.round(2)
end

puts hitung_cicilan(120_000_000, 0.3, 36)  # => 2333333.33

# Semua variabel lokal di atas tidak bisa diakses dari sini:
puts dp          # => NameError: undefined local variable or method 'dp'
puts sisa_bayar  # => NameError: undefined local variable or method 'sisa_bayar'

Scope Variabel Lokal di Dalam Blok #

Blok membuat scope baru untuk variabel lokal yang didefinisikan di dalamnya. Tapi variabel lokal yang sudah ada di luar blok tetap bisa diakses dari dalam blok — ini yang membedakannya dari scope di bahasa seperti Java atau C.

total    = 0
produk   = ["laptop", "mouse", "keyboard"]
harga    = [12_000_000, 350_000, 450_000]

produk.each_with_index do |nama, i|
  subtotal = harga[i]      # variabel lokal baru di dalam blok
  total += subtotal        # mengakses 'total' dari scope luar — valid
  puts "#{nama}: #{subtotal}"
end

puts "Total: #{total}"    # => Total: 12800000
puts subtotal             # => NameError: 'subtotal' hanya hidup di dalam blok

Block-local Variables #

Ruby menyediakan cara eksplisit untuk memastikan sebuah variabel di dalam blok tidak akan mengubah variabel dengan nama yang sama di scope luar — menggunakan tanda titik koma (;) dalam parameter blok.

nilai = 100

# ANTI-PATTERN: blok tanpa block-local variable
# variabel 'nilai' di dalam blok merujuk ke 'nilai' yang sama di luar
[1, 2, 3].each do |n|
  nilai = n * 10   # ← ini mengubah 'nilai' di scope luar!
end
puts nilai   # => 30  (bukan 100 seperti yang mungkin diharapkan)

# BENAR: deklarasikan sebagai block-local variable dengan ;
nilai = 100
[1, 2, 3].each do |n; nilai|   # 'nilai' setelah ; adalah variabel lokal blok
  nilai = n * 10               # ini TIDAK mengubah 'nilai' di luar
  puts nilai
end
puts nilai   # => 100  (tetap tidak berubah)

Multiple Assignment #

Ruby mendukung assignment beberapa variabel sekaligus dalam satu baris — fitur yang disebut parallel assignment atau destructuring.

# Assignment sederhana bersamaan
a, b, c = 1, 2, 3
puts "#{a}, #{b}, #{c}"   # => 1, 2, 3

# Swap dua variabel — cara idiomatik Ruby, tanpa variabel sementara
x, y = 10, 20
x, y = y, x
puts "x=#{x}, y=#{y}"    # => x=20, y=10

# Destructuring dari array
koordinat = [3.14, 2.71, 1.41]
pi, e, sqrt2 = koordinat
puts pi    # => 3.14
puts e     # => 2.71

# Splat operator untuk menangkap "sisa"
pertama, *sisanya = [1, 2, 3, 4, 5]
puts pertama.inspect    # => 1
puts sisanya.inspect    # => [2, 3, 4, 5]

*awal, terakhir = [1, 2, 3, 4, 5]
puts awal.inspect       # => [1, 2, 3, 4]
puts terakhir           # => 5

# Dari hash (Ruby 3.1+)
data = {nama: "Rina", kota: "Surabaya"}
nama, kota = data.values_at(:nama, :kota)
puts "#{nama} dari #{kota}"   # => Rina dari Surabaya

Variabel Instance #

Variabel instance adalah variabel yang melekat pada objek tertentu. Ia hidup selama objek itu hidup, dan setiap objek dari kelas yang sama memiliki salinan variabel instance-nya masing-masing yang benar-benar terpisah.

class Rekening
  def initialize(nomor, pemilik, saldo_awal = 0)
    @nomor   = nomor         # variabel instance
    @pemilik = pemilik       # variabel instance
    @saldo   = saldo_awal    # variabel instance
    @riwayat = []            # variabel instance — array kosong per objek
  end

  def setor(jumlah)
    @saldo   += jumlah
    @riwayat << {jenis: :setor, jumlah: jumlah, waktu: Time.now}
    self   # kembalikan objek untuk method chaining
  end

  def tarik(jumlah)
    raise "Saldo tidak mencukupi" if jumlah > @saldo
    @saldo   -= jumlah
    @riwayat << {jenis: :tarik, jumlah: jumlah, waktu: Time.now}
    self
  end

  def info
    "Rekening #{@nomor} | Pemilik: #{@pemilik} | Saldo: #{@saldo}"
  end
end

rek1 = Rekening.new("001", "Andi", 500_000)
rek2 = Rekening.new("002", "Budi", 1_000_000)

rek1.setor(200_000).setor(100_000)   # method chaining berkat return self
rek2.tarik(300_000)

puts rek1.info   # => Rekening 001 | Pemilik: Andi | Saldo: 800000
puts rek2.info   # => Rekening 002 | Pemilik: Budi | Saldo: 700000
# @saldo rek1 dan rek2 sepenuhnya terpisah

Nilai Default Variabel Instance #

Variabel instance yang belum diinisialisasi bernilai nil — bukan error. Ini bisa menjadi sumber bug yang halus jika tidak diantisipasi.

class Pengguna
  def tampilkan_nama
    # @nama belum tentu sudah diset — bisa nil
    puts @nama
  end

  def set_nama(nama)
    @nama = nama
  end
end

p = Pengguna.new
p.tampilkan_nama   # => nil (tidak error, tapi mungkin tidak diinginkan)
p.set_nama("Citra")
p.tampilkan_nama   # => Citra
# BENAR: inisialisasi semua variabel instance di initialize
class Pengguna
  def initialize(nama = nil)
    @nama     = nama
    @email    = nil
    @aktif    = false
    @login_at = nil
  end

  def tampilkan_nama
    @nama || "Pengguna Anonim"   # fallback jika nil
  end
end

Variabel Instance di Level Kelas #

Variabel instance tidak hanya bisa ada di level objek — kelas itu sendiri juga objek di Ruby, sehingga bisa punya variabel instance sendiri. Ini berbeda dari variabel kelas (@@).

class Konfigurasi
  # @timeout adalah variabel instance milik KELAS (bukan instance)
  @timeout = 30
  @max_retry = 3

  class << self
    attr_accessor :timeout, :max_retry
  end
end

puts Konfigurasi.timeout     # => 30
Konfigurasi.timeout = 60
puts Konfigurasi.timeout     # => 60

# Subkelas punya variabel instance-nya sendiri — tidak berbagi dengan induk
class KonfigurasiPremium < Konfigurasi
  @timeout = 120   # terpisah dari Konfigurasi.timeout
end

puts Konfigurasi.timeout       # => 60  (tidak berubah)
puts KonfigurasiPremium.timeout # => 120

Inilah keunggulan variabel instance di level kelas dibanding variabel kelas @@: ia tidak bocor ke subkelas.


Variabel Kelas #

Variabel kelas (@@) berbagi nilai di seluruh instance dari kelas yang sama, termasuk subkelasnya. Ini adalah karakteristik yang membuatnya berguna sekaligus berbahaya.

class Koneksi
  @@jumlah_aktif = 0
  @@batas_maks   = 10

  def initialize(host)
    raise "Batas koneksi tercapai!" if @@jumlah_aktif >= @@batas_maks
    @host = host
    @@jumlah_aktif += 1
    puts "Koneksi ke #{@host} dibuka. Aktif: #{@@jumlah_aktif}"
  end

  def tutup
    @@jumlah_aktif -= 1
    puts "Koneksi ke #{@host} ditutup. Aktif: #{@@jumlah_aktif}"
  end

  def self.jumlah_aktif
    @@jumlah_aktif
  end

  def self.batas_maks
    @@batas_maks
  end
end

k1 = Koneksi.new("db-primary")     # => Aktif: 1
k2 = Koneksi.new("db-replica")     # => Aktif: 2
k3 = Koneksi.new("cache-server")   # => Aktif: 3

puts Koneksi.jumlah_aktif   # => 3
k1.tutup                    # => Aktif: 2
puts Koneksi.jumlah_aktif   # => 2

Bahaya Variabel Kelas dengan Inheritance #

Perilaku variabel kelas yang paling sering menyebabkan bug adalah ia diwarisi — dan berbagi — dengan subkelas.

# ANTI-PATTERN: variabel kelas bocor ke subkelas
class Hewan
  @@jumlah = 0

  def initialize
    @@jumlah += 1
  end

  def self.jumlah
    @@jumlah
  end
end

class Kucing < Hewan; end
class Anjing < Hewan; end

Kucing.new
Kucing.new
Anjing.new

puts Hewan.jumlah    # => 3
puts Kucing.jumlah   # => 3  (bukan 2 seperti mungkin diharapkan!)
puts Anjing.jumlah   # => 3  (bukan 1 seperti mungkin diharapkan!)
# Semua subkelas berbagi @@jumlah yang sama dengan kelas induk

# BENAR: gunakan variabel instance di level kelas jika butuh state per-kelas
class Hewan
  @jumlah = 0

  def self.jumlah
    @jumlah
  end

  def self.inherited(subkelas)
    subkelas.instance_variable_set(:@jumlah, 0)
  end

  def initialize
    self.class.instance_variable_set(
      :@jumlah,
      self.class.instance_variable_get(:@jumlah) + 1
    )
  end
end

class Kucing < Hewan; end
class Anjing < Hewan; end

Kucing.new
Kucing.new
Anjing.new

puts Hewan.jumlah    # => 0  (hanya hewan yang dibuat langsung)
puts Kucing.jumlah   # => 2
puts Anjing.jumlah   # => 1

Variabel Global #

Variabel global diawali dengan $ dan bisa diakses dari mana saja dalam program — dalam method, kelas, modul, bahkan dari file yang berbeda setelah di-require.

$versi_aplikasi = "2.1.0"
$mode_debug     = false

def info_sistem
  puts "Versi: #{$versi_aplikasi}"
  puts "Debug: #{$mode_debug}"
end

class Logger
  def log(pesan)
    puts "[#{$versi_aplikasi}] #{pesan}" if $mode_debug
  end
end

info_sistem

Variabel Global Bawaan Ruby #

Ruby sendiri menggunakan variabel global untuk menyimpan informasi sistem yang penting. Ini adalah variabel global yang boleh digunakan karena memang dirancang untuk itu.

# $0 — nama file program yang sedang dijalankan
puts $0           # => variabel.rb (atau nama file yang dijalankan)

# $PROGRAM_NAME — alias yang lebih ekspresif untuk $0
puts $PROGRAM_NAME

# $LOAD_PATH ($:) — daftar direktori pencarian library
puts $LOAD_PATH.first(3).inspect

# $stdout, $stderr, $stdin — stream I/O standar
$stdout.puts "Ditulis ke stdout"
$stderr.puts "Ditulis ke stderr"

# $/ — separator baris (default: "\n")
# $\ — separator output (default: nil)
# $, — separator antar argumen puts (default: nil)

# $! — exception terakhir yang ditangkap
begin
  raise "Contoh error"
rescue => e
  puts $!.message   # => Contoh error (sama dengan e.message)
end

# $~ — hasil match regex terakhir
"Ruby 3.3" =~ /(\d+\.\d+)/
puts $~[0]    # => 3.3
puts $1       # => 3.3  ($1, $2, dst adalah capture groups)
Variabel global bawaan yang sering digunakan:
  $0 / $PROGRAM_NAME  → nama file yang dijalankan
  $LOAD_PATH / $:     → jalur pencarian library
  $stdout / $stderr   → stream output standar
  $stdin              → stream input standar
  $!                  → exception yang sedang aktif
  $@                  → backtrace exception saat ini
  $~                  → MatchData hasil regex terakhir
  $1 .. $9            → capture groups regex
  $/                  → input record separator

  ✗ Jangan buat variabel global sendiri kecuali benar-benar diperlukan
Variabel global buatan sendiri (bukan yang bawaan Ruby) hampir selalu bisa digantikan dengan solusi yang lebih baik: konstanta untuk nilai yang tidak berubah, variabel instance di level kelas untuk state per-kelas, atau parameter method untuk meneruskan nilai antar bagian kode. Variabel global membuat kode sulit diuji karena state-nya tersebar di seluruh program.

Shadowing Variabel #

Shadowing terjadi ketika variabel baru dengan nama yang sama dengan variabel di scope luar didefinisikan di scope yang lebih dalam, menutupi akses ke variabel aslinya.

nilai = 100

# Shadowing di dalam blok
[1, 2, 3].each do |nilai|   # parameter 'nilai' meng-shadow variabel luar
  puts nilai                  # mencetak 1, 2, 3 — bukan 100
end

puts nilai   # => 100  (variabel luar tidak berubah)

Shadowing bisa disengaja atau tidak disengaja. Yang berbahaya adalah shadowing yang tidak disengaja — ketika kamu menggunakan nama yang sudah dipakai di scope luar tanpa sadar.

# ANTI-PATTERN: shadowing tidak disengaja
def proses_pesanan(pesanan)
  total = 0

  pesanan.each do |item|
    harga = item[:harga]
    jumlah = item[:jumlah]
    total = harga * jumlah   # ← ini MENIMPA total di scope luar!
    # developer mungkin bermaksud: total += harga * jumlah
  end

  total   # hanya berisi kalkulasi item terakhir
end

# BENAR: nama yang berbeda untuk scope yang berbeda
def proses_pesanan(pesanan)
  total = 0

  pesanan.each do |item|
    harga   = item[:harga]
    jumlah  = item[:jumlah]
    subtotal = harga * jumlah   # nama berbeda, tidak ada konflik
    total += subtotal
  end

  total
end

Ruby akan mengeluarkan peringatan jika kamu mengaktifkan warning mode (ruby -w) dan ada shadowing yang terdeteksi:

ruby -w program.rb
# => warning: shadowing outer local variable - nilai

Aturan Penamaan dan Konvensi #

Ruby punya aturan penamaan variabel yang ketat (sintaksis) sekaligus konvensi yang kuat (komunitas).

Aturan Sintaksis #

# VALID — nama variabel yang diperbolehkan:
nama         = "Rani"
nama_lengkap = "Rani Putri"
_temp        = 42
__internal   = true
nama2        = "alias"

# TIDAK VALID — akan menyebabkan SyntaxError:
# 2nama     = "tidak boleh dimulai angka"
# nama-ku   = "tanda hubung bukan underscore"
# nama depan = "tidak boleh ada spasi"

Konvensi Komunitas #

# snake_case untuk variabel dan method — WAJIB di Ruby
total_harga     = 150_000
nama_pengguna   = "Budi"
is_aktif        = true      # atau gunakan aktif? untuk boolean (di method)

# ANTI-PATTERN: gaya dari bahasa lain yang tidak idiomatik di Ruby
totalHarga    = 150_000    # ✗ camelCase
NamaPengguna  = "Budi"     # ✗ PascalCase (dicadangkan untuk Kelas/Modul)
TOTAL_HARGA   = 150_000    # ✗ SCREAMING_SNAKE_CASE (dicadangkan Konstanta)

# Prefix underscore untuk variabel yang sengaja tidak digunakan
hasil_array.each do |nilai, _index|   # _index diabaikan, tapi eksplisit
  puts nilai
end

# Atau underscore tunggal untuk "throw-away"
array.each_with_index do |elemen, _|
  puts elemen
end

Penamaan yang Deskriptif #

# ANTI-PATTERN: nama terlalu singkat atau tidak bermakna
d  = 0.1
p  = 150_000
t  = d * p
r  = p - t

# BENAR: nama yang menjelaskan isi dan tujuan
diskon_persen  = 0.1
harga_awal     = 150_000
nilai_diskon   = diskon_persen * harga_awal
harga_akhir    = harga_awal - nilai_diskon

# ANTI-PATTERN: nama terlalu panjang dan redundan
nilai_dari_variabel_diskon_persen_yang_akan_dikalikan  = 0.1

# BENAR: singkat tapi bermakna — cukup untuk memahami konteksnya
diskon  = 0.1
Panduan panjang nama variabel:
  ✓ 1-2 kata untuk variabel iterasi singkat (i, n, key, val)
  ✓ 2-4 kata untuk variabel umum (total_harga, nama_pengguna)
  ✓ Lebih panjang jika konteks membutuhkan kejelasan
  ✗ Satu huruf kecuali dalam loop yang benar-benar trivial
  ✗ Singkatan yang ambigu (tmp, dat, cnt — gunakan temp, data, count)
  ✗ Nama yang menyesatkan (daftar yang sebenarnya Hash, dll)

Freeze — Variabel yang Tidak Bisa Diubah #

Di Ruby, kamu bisa “membekukan” nilai sebuah objek menggunakan method freeze. Setelah di-freeze, objek tidak bisa dimodifikasi.

kota = "Jakarta".freeze

kota.upcase    # => "JAKARTA" — aman, membuat objek baru
kota << " Selatan"   # => FrozenError: can't modify frozen String

# Berguna untuk string yang digunakan sebagai key atau identifier:
STATUS_AKTIF   = "aktif".freeze
STATUS_NONAKTIF = "nonaktif".freeze

# Dengan magic comment frozen_string_literal:
# frozen_string_literal: true
# Semua string literal di file otomatis frozen
sequenceDiagram
    participant K as Kode
    participant O as Objek
    participant M as Memori

    K->>M: nama = "Andi"
    M-->>O: Buat String "Andi" (mutable)
    K->>O: nama << " Wijaya"
    O-->>M: Modifikasi in-place → "Andi Wijaya"

    K->>M: nama = "Andi".freeze
    M-->>O: Buat String "Andi" (frozen)
    K->>O: nama << " Wijaya"
    O-->>K: FrozenError! Tidak bisa dimodifikasi

Ringkasan #

  • Prefix menentukan scope — huruf kecil (lokal), @ (instance), @@ (kelas), $ (global). Cukup lihat nama variabelnya untuk tahu di mana ia bisa diakses.
  • Variabel lokal terbatas pada method/blok — ia tidak bisa diakses dari scope yang lebih luar, tapi ia bisa mengakses variabel dari scope yang lebih luar.
  • Block-local variable dengan ; — gunakan |param; lokal| untuk mencegah variabel blok meng-shadow variabel luar secara tidak sengaja.
  • Multiple assignment dan splat — Ruby mendukung a, b = 1, 2 dan pertama, *sisa = array untuk destructuring yang ekspresif.
  • Variabel instance bernilai nil jika belum diinisialisasi — selalu inisialisasi semua variabel instance di initialize untuk menghindari bug halus.
  • Variabel kelas @@ bocor ke subkelas — gunakan variabel instance di level kelas (@var dalam class << self) untuk state per-kelas yang terisolasi.
  • Hindari variabel global buatan sendiri — hampir selalu ada solusi yang lebih baik: konstanta, parameter, atau variabel instance kelas.
  • snake_case adalah wajib — nama variabel lokal dan instance selalu snake_case. camelCase dan PascalCase dicadangkan untuk konteks lain.
  • Nama yang deskriptif lebih penting dari yang singkattotal_harga jauh lebih baik dari t, kecuali konteksnya memang trivial.
  • freeze untuk nilai yang tidak boleh berubah — gunakan pada string konstanta dan aktifkan # frozen_string_literal: true untuk performa lebih baik.

← Sebelumnya: Komentar   Berikutnya: Konstanta →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact