Konstanta

Konstanta #

Rust menyediakan dua kata kunci untuk mendefinisikan nilai yang tidak berubah di luar variabel biasa: const dan static. Keduanya terlihat serupa — keduanya global, keduanya tidak bisa di-reassign — tapi cara mereka bekerja di tingkat memori sangat berbeda, dan memilih yang salah bisa berdampak pada performa maupun keamanan program. Di atas itu, Rust juga punya const fn: fungsi yang bisa dievaluasi sepenuhnya saat kompilasi, menggeser komputasi yang biasanya terjadi di runtime ke fase build. Artikel ini membahas ketiga mekanisme ini secara mendalam — cara kerja, batasan, perbedaan, dan pola penggunaan yang tepat untuk masing-masing.

Mengapa Konstanta Penting #

Sebelum masuk ke sintaks, ada alasan yang lebih dalam mengapa konstanta — bukan sekadar variabel immutable biasa — penting dalam desain program.

Variabel immutable (let x = 5) punya scope terbatas: ia hidup dalam fungsi atau blok tempat ia dideklarasikan. Konstanta bisa dideklarasikan di tingkat modul atau crate, tersedia di mana saja, dan nilainya sudah diketahui saat kompilasi. Ini membuka beberapa manfaat:

// Tanpa konstanta — "magic number" tersebar di seluruh kodebase
fn hitung_kapasitas(jumlah: usize) -> usize {
    jumlah * 1024  // 1024 apa? Kenapa 1024?
}

fn validasi_ukuran(ukuran: usize) -> bool {
    ukuran <= 1024  // angka yang sama, tapi tidak ada jaminan konsistensi
}

// Dengan konstanta — satu sumber kebenaran, nama yang bermakna
const UKURAN_BLOK_BYTE: usize = 1024;

fn hitung_kapasitas(jumlah: usize) -> usize {
    jumlah * UKURAN_BLOK_BYTE
}

fn validasi_ukuran(ukuran: usize) -> bool {
    ukuran <= UKURAN_BLOK_BYTE
}

Jika suatu saat UKURAN_BLOK_BYTE perlu diubah ke 4096, cukup ubah satu baris — semua referensinya ikut berubah. Tanpa konstanta, kamu harus mencari dan mengganti setiap kemunculan angka 1024 secara manual, dengan risiko melewatkan beberapa.


Konstanta const #

const mendefinisikan nilai yang sepenuhnya dievaluasi saat kompilasi. Compiler tidak mengalokasikan satu slot memori untuk konstanta — ia menyisipkan (inline) nilai langsung ke setiap tempat konstanta digunakan, seperti copy-paste yang dilakukan compiler secara otomatis.

Sintaks dan Aturan Dasar #

// Deklarasi di tingkat modul — paling umum
const NAMA_KONSTANTA: Tipe = ekspresi_konstan;

// Contoh nyata
const PI: f64 = 3.141_592_653_589_793;
const MAKS_KONEKSI: u32 = 100;
const NAMA_APP: &str = "RustApp";
const VERSI: (u8, u8, u8) = (1, 0, 0); // tuple juga valid

Tiga aturan yang tidak bisa dilanggar untuk const:

  1. Tipe harus selalu eksplisit — tidak ada type inference untuk const
  2. Nilai harus constant expression — harus bisa dievaluasi sepenuhnya saat kompilasi tanpa runtime
  3. Tidak bisa mutconst mut bukan sintaks yang valid di Rust
// ANTI-PATTERN: tidak ada anotasi tipe
const MAKS = 100; // error[E0121]: type annotations needed

// ANTI-PATTERN: nilai yang tidak bisa dievaluasi saat kompilasi
const WAKTU_SEKARANG: u64 = std::time::SystemTime::now()
    .duration_since(std::time::UNIX_EPOCH)
    .unwrap()
    .as_secs(); // error: cannot call non-const fn `SystemTime::now` in constants

// BENAR: literal atau ekspresi yang sepenuhnya konstan
const MAKS: u32 = 100;
const DUA_KALI_MAKS: u32 = MAKS * 2; // menggunakan konstanta lain — valid

Scope Konstanta #

const bisa dideklarasikan di scope manapun: global (tingkat modul/crate), di dalam fungsi, bahkan di dalam blok. Scope dalam fungsi berguna untuk konstanta yang relevan hanya dalam konteks lokal.

const GRAVITASI: f64 = 9.81; // tersedia di seluruh modul

fn hitung_energi_potensial(massa_kg: f64, ketinggian_m: f64) -> f64 {
    const SATUAN: &str = "Joule"; // hanya relevan di sini
    let energi = massa_kg * GRAVITASI * ketinggian_m;
    println!("Energi potensial: {} {}", energi, SATUAN);
    energi
}

fn main() {
    hitung_energi_potensial(70.0, 10.0);
    println!("Gravitasi bumi: {} m/s²", GRAVITASI);
    // println!("{}", SATUAN); // error: SATUAN tidak tersedia di sini
}

Konstanta dalam Implementasi Struct #

const bisa dideklarasikan di dalam blok impl — berguna untuk nilai yang terkait erat dengan tipe tertentu:

struct Lingkaran {
    radius: f64,
}

impl Lingkaran {
    const PI: f64 = std::f64::consts::PI;

    fn luas(&self) -> f64 {
        Self::PI * self.radius * self.radius
    }

    fn keliling(&self) -> f64 {
        2.0 * Self::PI * self.radius
    }
}

struct Persegi {
    sisi: f64,
}

impl Persegi {
    // Rasio diagonal terhadap sisi: √2
    const RASIO_DIAGONAL: f64 = std::f64::consts::SQRT_2;

    fn diagonal(&self) -> f64 {
        self.sisi * Self::RASIO_DIAGONAL
    }
}

fn main() {
    let l = Lingkaran { radius: 5.0 };
    println!("Luas: {:.4}", l.luas());
    println!("Keliling: {:.4}", l.keliling());

    let p = Persegi { sisi: 10.0 };
    println!("Diagonal: {:.4}", p.diagonal());
}

Konstanta dalam Trait #

const juga bisa menjadi bagian dari definisi trait — setiap tipe yang mengimplementasikan trait bisa memberikan nilai konstanta yang berbeda:

trait Batas {
    const MIN: i32;
    const MAKS: i32;

    fn dalam_batas(&self, nilai: i32) -> bool;
}

struct SuhuCelsius;
struct SuhuFahrenheit;

impl Batas for SuhuCelsius {
    const MIN: i32 = -273; // nol absolut dalam Celsius
    const MAKS: i32 = 1_000_000; // perkiraan suhu inti bintang

    fn dalam_batas(&self, nilai: i32) -> bool {
        nilai >= Self::MIN && nilai <= Self::MAKS
    }
}

impl Batas for SuhuFahrenheit {
    const MIN: i32 = -459; // nol absolut dalam Fahrenheit
    const MAKS: i32 = 1_800_032;

    fn dalam_batas(&self, nilai: i32) -> bool {
        nilai >= Self::MIN && nilai <= Self::MAKS
    }
}

fn main() {
    let c = SuhuCelsius;
    println!("Suhu 100°C valid: {}", c.dalam_batas(100));
    println!("Suhu -300°C valid: {}", c.dalam_batas(-300));
    println!("Batas bawah Celsius: {}°C", SuhuCelsius::MIN);
}

Konstanta static #

static mendefinisikan nilai global yang memiliki satu lokasi memori tetap sepanjang masa hidup program. Berbeda dari const yang di-inline, nilai static benar-benar ada sebagai satu objek di memori — kamu bisa mengambil referensinya, dan referensi itu akan selalu valid selama program berjalan.

static Immutable #

static NAMA_VERSI: &str = "1.0.0-beta";
static BUFFER_DEFAULT: [u8; 8] = [0; 8];
static TABEL_KODE: [(u8, &str); 3] = [
    (200, "OK"),
    (404, "Not Found"),
    (500, "Internal Server Error"),
];

fn main() {
    println!("Versi: {}", NAMA_VERSI);

    // Mengambil referensi ke static — referensi punya lifetime 'static
    let r: &'static str = NAMA_VERSI;
    println!("Referensi: {}", r);

    for (kode, pesan) in &TABEL_KODE {
        println!("{}: {}", kode, pesan);
    }
}

Lifetime 'static yang muncul di sini berarti referensi valid selama program berjalan — tidak ada kemungkinan referensi menjadi dangling karena data staticnya selalu ada.

Kapan static Lebih Tepat dari const #

Gunakan static (immutable) daripada const ketika:

// BENAR menggunakan static: data besar yang tidak perlu di-copy di setiap penggunaan
static DAFTAR_NEGARA: &[&str] = &[
    "Indonesia", "Malaysia", "Singapura", "Thailand", "Vietnam",
    "Filipina", "Myanmar", "Kamboja", "Laos", "Brunei",
    // ... ratusan negara lainnya
];

// Dengan const, nilai ini akan di-copy ke setiap titik penggunaan
// Dengan static, semua referensi menunjuk ke satu lokasi yang sama

// BENAR menggunakan const: nilai kecil yang sering digunakan
const MAKS_RETRY: u8 = 3;
// Di-inline = tidak ada overhead indirection, langsung nilai literal
flowchart TD
    A{Perlu konstanta?}
    A --> B{Data berukuran besar\natau perlu referensi\ndengan lifetime statis?}
    B -- Ya --> C[Gunakan static\nSatu lokasi memori\nReferensi &'static T valid]
    B -- Tidak --> D{Nilai perlu dihitung\ndari ekspresi kompleks\nsaat kompilasi?}
    D -- Ya --> E[Gunakan const fn\n+ const]
    D -- Tidak --> F[Gunakan const\nDi-inline oleh compiler\nPaling efisien untuk nilai kecil]

static mut — Global Mutable State #

Rust mengizinkan static mut untuk mendefinisikan state global yang bisa diubah, tapi dengan satu syarat ketat: semua akses harus berada dalam blok unsafe.

static mut JUMLAH_PANGGILAN: u32 = 0;

fn catat_panggilan() {
    unsafe {
        JUMLAH_PANGGILAN += 1;
    }
}

fn baca_jumlah() -> u32 {
    unsafe { JUMLAH_PANGGILAN }
}

fn main() {
    catat_panggilan();
    catat_panggilan();
    catat_panggilan();
    println!("Dipanggil {} kali", baca_jumlah()); // 3
}
Hindari static mut dalam kode produksi. Setiap akses ke static mut adalah unsafe karena tidak ada jaminan keamanan konkuren — dua thread yang mengakses variabel yang sama bersamaan tanpa sinkronisasi adalah data race, perilaku tidak terdefinisi (undefined behavior) di Rust. Untuk state global yang perlu diubah, gunakan alternatif yang aman: Mutex<T>, RwLock<T>, atau tipe atomik dari std::sync::atomic.

Alternatif Aman untuk static mut #

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Mutex;

// Pengganti static mut untuk counter — tipe atomik, aman tanpa unsafe
static JUMLAH_PANGGILAN: AtomicU32 = AtomicU32::new(0);

// Pengganti static mut untuk data kompleks — Mutex
static KONFIGURASI: Mutex<Option<String>> = Mutex::new(None);

fn catat_panggilan() {
    // Tidak butuh unsafe — operasi atomik sudah thread-safe
    JUMLAH_PANGGILAN.fetch_add(1, Ordering::SeqCst);
}

fn set_konfigurasi(nilai: &str) {
    // Tidak butuh unsafe — Mutex menjamin akses eksklusif
    let mut lock = KONFIGURASI.lock().unwrap();
    *lock = Some(nilai.to_string());
}

fn main() {
    catat_panggilan();
    catat_panggilan();
    set_konfigurasi("mode=produksi");

    println!("Panggilan: {}", JUMLAH_PANGGILAN.load(Ordering::SeqCst));
    println!("Config: {:?}", KONFIGURASI.lock().unwrap());
}

const fn — Komputasi Waktu Kompilasi #

const fn adalah fungsi yang bisa dievaluasi saat kompilasi jika semua argumennya adalah nilai konstan. Hasilnya bisa digunakan untuk mendefinisikan const dari ekspresi yang lebih kompleks dari sekadar literal.

Fungsi const Dasar #

const fn kilo(n: u64) -> u64 {
    n * 1_000
}

const fn mega(n: u64) -> u64 {
    kilo(n) * 1_000 // memanggil const fn lain — valid
}

const fn giga(n: u64) -> u64 {
    mega(n) * 1_000
}

// Semua ini dievaluasi saat kompilasi — tidak ada overhead runtime
const SATU_KB: u64 = kilo(1);
const SATU_MB: u64 = mega(1);
const SATU_GB: u64 = giga(1);
const BATAS_FILE: u64 = mega(512); // 512 MB dalam byte

fn main() {
    println!("1 KB = {} byte", SATU_KB);
    println!("1 MB = {} byte", SATU_MB);
    println!("1 GB = {} byte", SATU_GB);
    println!("Batas ukuran file: {} byte", BATAS_FILE);
}

const fn dengan Logika Kondisional #

Sejak Rust 1.46, const fn mendukung if, else, dan loop sederhana:

const fn maks(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

const fn min(a: i32, b: i32) -> i32 {
    if a < b { a } else { b }
}

const fn clamp(nilai: i32, bawah: i32, atas: i32) -> i32 {
    maks(bawah, min(nilai, atas))
}

const fn faktorial(n: u64) -> u64 {
    // Loop juga valid di const fn sejak Rust 1.46
    let mut hasil = 1u64;
    let mut i = 2u64;
    while i <= n {
        hasil *= i;
        i += 1;
    }
    hasil
}

// Semua dihitung saat kompilasi
const BATAS_BAWAH: i32 = maks(-100, 0);    // 0
const BATAS_ATAS: i32 = min(1000, 500);     // 500
const NILAI_VALID: i32 = clamp(750, 0, 500); // 500
const FAKTORIAL_10: u64 = faktorial(10);     // 3628800

fn main() {
    println!("Batas bawah: {}", BATAS_BAWAH);
    println!("Batas atas: {}", BATAS_ATAS);
    println!("Nilai setelah clamp: {}", NILAI_VALID);
    println!("10! = {}", FAKTORIAL_10);
}

Batasan const fn #

Tidak semua operasi bisa dilakukan di dalam const fn. Batasan ini ada karena beberapa operasi secara inheren membutuhkan runtime:

// TIDAK BISA di const fn:
// - Alokasi heap (Box::new, Vec::new, String::from, dll.)
// - Floating-point aritmetika (di beberapa versi Rust lama)
// - Trait objects (dyn Trait)
// - Closures (di beberapa konteks)
// - Penanganan exception/panic kompleks

const fn contoh_valid(n: u32) -> u32 {
    // ✓ Aritmetika integer
    let hasil = n * 2 + 1;

    // ✓ Kondisional
    if hasil > 100 { 100 } else { hasil }
}

// ANTI-PATTERN: mencoba alokasi heap di const fn
const fn buat_string() -> String {
    String::from("halo") // error: cannot call non-const fn in constants
}

const fn sebagai Fungsi Biasa #

Fungsi yang ditandai const tetap bisa dipanggil di runtime seperti fungsi biasa — ia hanya juga bisa dievaluasi saat kompilasi:

const fn pangkat_dua(n: u32) -> u32 {
    n * n
}

const LIMA_KUADRAT: u32 = pangkat_dua(5); // compile-time: 25

fn main() {
    let input: u32 = 7; // nilai runtime
    let hasil = pangkat_dua(input); // runtime call — tetap valid
    println!("7² = {}", hasil);
    println!("5² = {}", LIMA_KUADRAT);
}

Perbandingan const, static, dan Variabel let #

Aspekconststaticlet (immutable)
LokasiDi-inline (tidak ada slot memori tetap)Satu lokasi memori tetapStack frame fungsi
ScopeModul, fungsi, blok, implGlobal (crate-wide)Scope lokal
Tipe wajib eksplisitYaYaTidak (bisa diinfer)
Bisa mutableTidakYa (static mut, tapi berbahaya)Ya (let mut)
LifetimeN/A (tidak ada lokasi memori)'staticTergantung scope
EvaluasiKompilasiKompilasiRuntime
ReferensiSetiap inline punya referensi berbedaSatu referensi &'static TReferensi lokal
Cocok untukNilai kecil yang sering dipakaiData besar / perlu referensi statisData lokal sementara

Konvensi Penamaan #

Rust menggunakan SCREAMING_SNAKE_CASE untuk semua konstanta — ini konvensi yang ditegakkan oleh linter clippy dan diikuti oleh seluruh ekosistem Rust termasuk standard library:

// ✓ BENAR: SCREAMING_SNAKE_CASE
const MAKS_UKURAN_BUFFER: usize = 4096;
const TIMEOUT_KONEKSI_MS: u64 = 5_000;
static KUNCI_ENKRIPSI: &[u8] = b"kunci-rahasia-32-karakter-panjang";

// ✗ ANTI-PATTERN: menggunakan konvensi lain
const maxUkuranBuffer: usize = 4096;  // camelCase — warning dari clippy
const max_ukuran_buffer: usize = 4096; // snake_case — warning dari clippy
const MaxUkuranBuffer: usize = 4096;  // PascalCase — warning dari clippy

Underscore sebagai pemisah digit sangat dianjurkan untuk angka besar — jauh lebih mudah dibaca:

// ✗ Sulit dibaca
const POPULASI_BUMI: u64 = 8000000000;

// ✓ Mudah dibaca
const POPULASI_BUMI: u64 = 8_000_000_000;
const SATU_JUTA: u32 = 1_000_000;
const BATAS_PORT: u16 = 65_535;

Pola Penggunaan Nyata #

Konfigurasi Aplikasi #

// config.rs — semua konstanta konfigurasi di satu tempat
pub const VERSI_API: &str = "v2";
pub const HOST_DEFAULT: &str = "127.0.0.1";
pub const PORT_DEFAULT: u16 = 8080;
pub const MAKS_KONEKSI_DB: u32 = 20;
pub const TIMEOUT_REQUEST_MS: u64 = 30_000;
pub const MAKS_UKURAN_BODY_BYTES: usize = 10 * 1024 * 1024; // 10 MB

fn main() {
    println!(
        "Server berjalan di {}:{} (API {})",
        HOST_DEFAULT, PORT_DEFAULT, VERSI_API
    );
    println!("Timeout: {} ms", TIMEOUT_REQUEST_MS);
    println!("Maks body: {} byte", MAKS_UKURAN_BODY_BYTES);
}

Tabel Lookup Kompilasi #

// Tabel sin untuk sudut 0°, 30°, 45°, 60°, 90° — dihitung sekali saat kompilasi
const TABEL_SIN: [f64; 5] = [0.0, 0.5, 0.7071067811865476, 0.8660254037844386, 1.0];
const SUDUT_DERAJAT: [u32; 5] = [0, 30, 45, 60, 90];

fn main() {
    for (sudut, sin) in SUDUT_DERAJAT.iter().zip(TABEL_SIN.iter()) {
        println!("sin({}°) = {:.4}", sudut, sin);
    }
}

Array Berukuran dari Konstanta #

Salah satu keunggulan const yang tidak bisa dilakukan variabel biasa — menggunakannya sebagai ukuran array:

const KAPASITAS_BUFFER: usize = 256;
const JUMLAH_WORKER: usize = 4;

fn main() {
    // Ukuran array harus diketahui saat kompilasi — const memungkinkan ini
    let buffer: [u8; KAPASITAS_BUFFER] = [0; KAPASITAS_BUFFER];
    let worker_ids: [usize; JUMLAH_WORKER] = [0, 1, 2, 3];

    println!("Buffer: {} byte", buffer.len());
    println!("Workers: {:?}", worker_ids);

    // ANTI-PATTERN: menggunakan variabel let sebagai ukuran array
    let kapasitas = 256;
    // let buffer2: [u8; kapasitas] = [0; kapasitas]; // error: expected constant, found local variable
}

Ringkasan #

  • const di-inline oleh compiler — tidak ada lokasi memori tetap; nilai disalin ke setiap titik penggunaan. Ideal untuk nilai kecil seperti angka, string pendek, dan tuple.
  • static punya satu lokasi memori — semua referensi ke static menunjuk ke tempat yang sama. Gunakan untuk data besar atau ketika kamu butuh referensi &'static T.
  • Tipe selalu wajib eksplisit untuk const dan static — tidak ada type inference seperti pada let.
  • const mut tidak adaconst tidak bisa mutable sama sekali. static mut ada tapi berbahaya dan membutuhkan unsafe untuk setiap akses.
  • Hindari static mut — gunakan AtomicT untuk counter/flag, Mutex<T> atau RwLock<T> untuk state kompleks yang perlu diubah dari banyak tempat.
  • const fn memindahkan komputasi ke compile-time — fungsi bisa dievaluasi saat kompilasi jika argumennya konstan. Mendukung if-else dan while sejak Rust 1.46.
  • const bisa digunakan sebagai ukuran array — ini salah satu kelebihan utama const dibanding variabel let biasa.
  • Gunakan SCREAMING_SNAKE_CASE untuk semua konstanta — ini konvensi resmi Rust yang ditegakkan oleh clippy.
  • Underscore sebagai pemisah digit (1_000_000) sangat dianjurkan untuk angka besar agar mudah dibaca.

← Sebelumnya: Variabel   Berikutnya: Tipe Data →

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