Variabel dan Mutabilitas
Kayak yang udah disebutin di bagian “Menyimpan Nilai dengan Variabel”, secara default, variabel itu immutable (nggak bisa diubah). Ini salah satu cara Rust “nyenggol” kita buat nulis kode yang manfaatin keamanan dan kemudahan concurrency yang ditawarin Rust. Tapi, kita tetep punya opsi buat bikin variabel jadi mutable (bisa diubah). Yuk kita eksplor gimana dan kenapa Rust nyaranin kita buat lebih milih immutability, dan kenapa kadang kita malah mau milih buat nggak pakenya.
Pas sebuah variabel itu immutable, sekali nilainya di-bind ke sebuah nama,
kita nggak bisa ngerubah nilai itu. Buat gambarin ini, coba bikin project baru
namanya variables di direktori projects kita pake cargo new variables.
Terus, di direktori variables yang baru, buka src/main.rs terus ganti kodenya jadi kayak gini, yang sebenernya belum bisa di-compile sekarang:
Nama file: src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Simpan terus jalanin programnya pake cargo run. Kita bakal dapet pesan error
soal immutability error, kayak yang ditunjukin di output ini:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
Contoh ini nunjukin gimana compiler ngebantu kita nemuin error di program kita. Error dari compiler emang kadang bikin kesel, tapi sebenernya itu cuma berarti program kita belum aman buat ngelakuin apa yang kita mau; itu bukan berarti kita bukan programmer yang jago! Para Rustacean yang udah pro pun tetep sering dapet error dari compiler.
Kita dapet pesan error cannot assign twice to immutable variable `x`
karena kita nyoba buat ngasih nilai kedua ke variabel x yang immutable.
Penting sekali buat kita dapet compile-time error pas kita nyoba ngerubah nilai yang udah ditentuin sebagai immutable karena situasi ini bisa memicu bug. Kalau satu bagian kode kita jalan dengan asumsi kalau sebuah nilai nggak bakal berubah, terus bagian kode lain malah ngerubah nilai itu, ada kemungkinan bagian pertama tadi nggak bakal jalan sesuai desainnya. Penyebab bug kayak gini bisa susah sekali dilacak setelah kejadian, apalagi kalau bagian kode kedua ngerubah nilainya cuma “kadang-kadang” doang. Compiler Rust ngejamin kalau pas kita bilang sebuah nilai nggak bakal berubah, ya dia benar-benar nggak bakal berubah, jadi kita nggak perlu repot-repot jagain sendiri. Kode kita jadi lebih gampang buat dipahamin alurnya.
Tapi mutability emang bisa sangat berguna, dan bisa bikin kode lebih nyaman
buat ditulis. Walaupun variabel itu immutable secara default, kita bisa bikin
mereka jadi mutable dengan nambahin mut di depan nama variabelnya kayak yang
kita lakuin di Bab 2. Nambahin mut juga
ngasih tau maksud (intent) kita ke orang yang baca kode kita nanti kalau bagian
lain dari kode bakal ngerubah nilai variabel ini.
Contohnya, yuk kita ubah src/main.rs jadi kayak gini:
Nama file: src/main.rs
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Pas kita jalanin programnya sekarang, hasilnya kayak gini:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
Kita diperbolehkan buat ngerubah nilai yang di-bind ke x dari 5 jadi 6
pas mut dipake. Akhirnya, keputusan buat pake mutability atau nggak itu
balik lagi ke kita dan tergantung apa yang menurut kita paling jelas di situasi
tertentu itu.
Konstanta (Constants)
Kayak variabel immutable, konstanta adalah nilai yang di-bind ke sebuah nama dan nggak boleh berubah, tapi ada beberapa perbedaan antara konstanta sama variabel.
Pertama, kita nggak boleh pake mut sama konstanta. Konstanta nggak cuma
immutable secara default—mereka selalu immutable. Kita mendeklarasikan
konstanta pake keyword const bukannya let, dan tipe nilainya harus
diannotasi. Kita bakal bahas soal tipe dan annotasi tipe di bagian selanjutnya,
“Tipe Data”, jadi nggak usah pusing dulu soal detailnya sekarang.
Pokoknya tau aja kalau kita harus selalu nulis tipenya.
Konstanta bisa dideklarasikan di scope mana pun, termasuk scope global, yang bikin mereka berguna buat nilai yang perlu diketahuin sama banyak bagian kode.
Perbedaan terakhir adalah konstanta cuma boleh di-set ke constant expression, bukan hasil dari nilai yang cuma bisa dihitung pas runtime.
Ini contoh deklarasi konstanta:
#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}
Nama konstantanya adalah THREE_HOURS_IN_SECONDS dan nilainya di-set ke hasil
perkalian 60 (jumlah detik dalam satu menit) dikali 60 (jumlah menit dalam satu
jam) dikali 3 (jumlah jam yang mau kita itung di program ini). Konvensi
penamaan Rust buat konstanta adalah pake huruf kapital semua (uppercase) dengan
garis bawah (underscore) di antara kata-katanya. Compiler bisa nge-evaluasi
sekumpulan operasi terbatas pas compile time, yang bikin kita bisa milih buat
nulis nilai ini dengan cara yang lebih gampang dipahamin dan diverifikasi,
bukannya langsung nulis nilai 10.800. Liat bagian Rust Reference soal constant
evaluation buat info lebih lanjut soal operasi apa aja yang bisa
dipake pas deklarasi konstanta.
Konstanta itu valid selama program jalan, di dalem scope tempat mereka dideklarasikan. Sifat ini bikin konstanta berguna buat nilai di domain aplikasi kita yang mungkin perlu diketahuin sama banyak bagian program, kayak jumlah poin maksimal yang boleh didapet player sebuah game, atau kecepatan cahaya.
Ngambil nilai hardcoded yang dipake di seluruh program terus dikasih nama sebagai konstanta itu sangat berguna buat nyampein makna nilai itu ke orang yang bakal maintain kodenya nanti. Ini juga ngebantu biar cuma ada satu tempat di kode kita yang perlu diubah kalau nilai hardcoded itu perlu di-update di masa depan.
Shadowing
Kayak yang kita liat di tutorial game tebak angka di Bab 2,
kita bisa mendeklarasikan variabel baru dengan nama yang sama kayak variabel
sebelumnya. Para Rustacean bilang kalau variabel pertama itu di-shadow
(dibayangi) sama variabel kedua, yang artinya variabel kedua lah yang bakal
diliat sama compiler pas kita pake nama variabel itu. Efektifnya, variabel
kedua menutupi variabel pertama, ngambil semua penggunaan nama variabel itu buat
dirinya sendiri sampe dia sendiri di-shadow atau scope-nya abis. Kita bisa
nge-shadow sebuah variabel dengan pake nama variabel yang sama dan ngulangin
penggunaan keyword let kayak gini:
Nama file: src/main.rs
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
Program ini pertama-tama nge-bind x ke nilai 5. Terus dia bikin variabel
baru x dengan ngulangin let x =, ngambil nilai aslinya terus ditambahin 1
biar nilai x jadi 6. Terus, di dalem scope dalem yang dibuat pake kurung
kurawal, statement let yang ketiga juga nge-shadow x dan bikin variabel
baru, ngaliin nilai sebelumnya sama 2 biar x jadi 12. Pas scope itu abis,
shadowing dalemnya kelar dan x balik lagi jadi 6. Pas kita jalanin
program ini, output-nya bakal kayak gini:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
Shadowing itu beda sama nandain variabel sebagai mut karena kita bakal dapet
compile-time error kalau kita nggak sengaja nyoba buat nge-assign ulang ke
variabel ini tanpa pake keyword let. Dengan pake let, kita bisa ngelakuin
beberapa transformasi pada sebuah nilai tapi tetep bikin variabelnya jadi
immutable setelah transformasi itu selesai.
Perbedaan lain antara mut sama shadowing adalah karena kita sebenernya bikin
variabel baru pas kita pake keyword let lagi, kita bisa ngerubah tipe nilainya
tapi tetep pake nama yang sama. Misalnya, katakanlah program kita minta user
buat nunjukin berapa banyak spasi yang mereka mau di antara teks dengan masukin
karakter spasi, terus kita mau nyimpen input itu sebagai angka:
fn main() {
let spaces = " ";
let spaces = spaces.len();
}
Variabel spaces yang pertama itu tipe string dan variabel spaces yang kedua
itu tipe angka. Jadi shadowing bikin kita nggak perlu repot mikirin nama yang
beda, kayak spaces_str dan spaces_num; mendingan kita pake lagi nama
spaces yang lebih simpel. Tapi, kalau kita nyoba pake mut buat hal ini,
kayak yang ditunjukin di sini, kita bakal dapet compile-time error:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
Error-nya bilang kalau kita nggak diperbolehkan buat nge-mutasi tipe variabel:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
Sekarang setelah kita eksplor gimana cara kerja variabel, yuk kita liat tipe data lainnya yang bisa mereka punya.