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:
- Tipe harus selalu eksplisit — tidak ada type inference untuk
const - Nilai harus constant expression — harus bisa dievaluasi sepenuhnya saat kompilasi tanpa runtime
- Tidak bisa
mut—const mutbukan 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
}
Hindaristatic mutdalam kode produksi. Setiap akses kestatic mutadalahunsafekarena 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 daristd::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
#
| Aspek | const | static | let (immutable) |
|---|---|---|---|
| Lokasi | Di-inline (tidak ada slot memori tetap) | Satu lokasi memori tetap | Stack frame fungsi |
| Scope | Modul, fungsi, blok, impl | Global (crate-wide) | Scope lokal |
| Tipe wajib eksplisit | Ya | Ya | Tidak (bisa diinfer) |
| Bisa mutable | Tidak | Ya (static mut, tapi berbahaya) | Ya (let mut) |
| Lifetime | N/A (tidak ada lokasi memori) | 'static | Tergantung scope |
| Evaluasi | Kompilasi | Kompilasi | Runtime |
| Referensi | Setiap inline punya referensi berbeda | Satu referensi &'static T | Referensi lokal |
| Cocok untuk | Nilai kecil yang sering dipakai | Data besar / perlu referensi statis | Data 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 #
constdi-inline oleh compiler — tidak ada lokasi memori tetap; nilai disalin ke setiap titik penggunaan. Ideal untuk nilai kecil seperti angka, string pendek, dan tuple.staticpunya satu lokasi memori — semua referensi kestaticmenunjuk ke tempat yang sama. Gunakan untuk data besar atau ketika kamu butuh referensi&'static T.- Tipe selalu wajib eksplisit untuk
constdanstatic— tidak ada type inference seperti padalet.const muttidak ada —consttidak bisa mutable sama sekali.static mutada tapi berbahaya dan membutuhkanunsafeuntuk setiap akses.- Hindari
static mut— gunakanAtomicTuntuk counter/flag,Mutex<T>atauRwLock<T>untuk state kompleks yang perlu diubah dari banyak tempat.const fnmemindahkan komputasi ke compile-time — fungsi bisa dievaluasi saat kompilasi jika argumennya konstan. Mendukung if-else dan while sejak Rust 1.46.constbisa digunakan sebagai ukuran array — ini salah satu kelebihan utamaconstdibanding variabelletbiasa.- Gunakan
SCREAMING_SNAKE_CASEuntuk 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.