Seleksi Kondisi #
Seleksi kondisi di Rust terasa familiar di permukaan — ada if, ada else — tapi dua hal membedakannya secara fundamental dari bahasa lain. Pertama, if dan match di Rust adalah expression, bukan statement: keduanya menghasilkan nilai yang bisa langsung diassign ke variabel. Kedua, match di Rust jauh lebih kuat dari switch di bahasa lain — ia mendukung destructuring, guard condition, binding nama, dan yang paling penting, ia exhaustive: compiler menolak program yang tidak menangani semua kemungkinan pattern. Kombinasi keduanya menghasilkan kode kondisional yang lebih ekspresif, lebih aman, dan lebih ringkas dari if-else berantai.
Ekspresi if
#
if di Rust membutuhkan kondisi bertipe bool secara eksak — tidak ada konversi implisit dari integer atau pointer ke boolean seperti di C:
fn main() {
let suhu = 32;
// Bentuk dasar
if suhu > 30 {
println!("Panas");
}
// Dengan else
if suhu > 30 {
println!("Panas");
} else {
println!("Sejuk");
}
// ANTI-PATTERN: kondisi non-bool (tidak valid di Rust)
let angka = 1;
// if angka { ... } // error[E0308]: expected `bool`, found integer
// if angka != 0 { ... } // BENAR: eksplisit
// ANTI-PATTERN: tidak perlu == true atau == false
let aktif = true;
if aktif == true { println!("Ini berlebihan"); } // ✗
if aktif { println!("Lebih idiomatis"); } // ✓
if !aktif { println!("Tidak aktif"); } // ✓
}
if sebagai Expression
#
Ini salah satu fitur yang paling sering mengejutkan developer yang datang dari bahasa lain: if di Rust adalah expression yang menghasilkan nilai. Seluruh blok if-else bisa diletakkan di sisi kanan assignment.
fn main() {
let skor = 85;
// if sebagai expression — menggantikan operator ternary ?: dari bahasa lain
let kelulusan = if skor >= 75 { "Lulus" } else { "Tidak Lulus" };
println!("Status: {}", kelulusan);
// Bisa lebih panjang dari satu ekspresi — nilai terakhir adalah hasilnya
let kategori = if skor >= 90 {
"Sangat Baik"
} else if skor >= 80 {
"Baik"
} else if skor >= 70 {
"Cukup"
} else {
"Kurang"
};
println!("Kategori: {}", kategori);
// ANTI-PATTERN: tipe berbeda di setiap branch
// let nilai = if skor >= 75 { "Lulus" } else { 0 };
// error[E0308]: `if` and `else` have incompatible types
// Semua branch HARUS mengembalikan tipe yang sama
// BENAR: pastikan tipe konsisten
let pesan = if skor >= 75 {
format!("Lulus dengan skor {}", skor)
} else {
format!("Tidak lulus, skor {} kurang dari 75", skor)
};
println!("{}", pesan);
}
if-else if Berantai
#
fn klasifikasi_bmi(bmi: f64) -> &'static str {
if bmi < 18.5 {
"Kurus"
} else if bmi < 25.0 {
"Normal"
} else if bmi < 30.0 {
"Gemuk"
} else {
"Obesitas"
}
}
fn main() {
let berat = 70.0_f64;
let tinggi = 1.75_f64;
let bmi = berat / (tinggi * tinggi);
println!("BMI: {:.1} — {}", bmi, klasifikasi_bmi(bmi));
// ANTI-PATTERN: terlalu banyak else-if untuk pattern yang bisa ditangani match
// Jika kamu memeriksa nilai yang sama berulang kali, match lebih tepat
}
match — Pattern Matching yang Exhaustive
#
match adalah konstruksi paling kuat di Rust untuk seleksi kondisi. Ia memaksa kamu menangani semua kemungkinan — jika ada pattern yang tidak ditangani, kode tidak akan dikompilasi. Ini mencegah bug kelas “lupa menangani kasus X” secara total.
fn main() {
let angka = 7;
match angka {
1 => println!("Satu"),
2 => println!("Dua"),
3 => println!("Tiga"),
_ => println!("Lainnya"), // wildcard — wajib jika tidak exhaustive
}
// match juga expression — menghasilkan nilai
let deskripsi = match angka {
1 => "satu",
2 => "dua",
3 => "tiga",
_ => "banyak",
};
println!("Angka {} = {}", angka, deskripsi);
}
Pattern Majemuk, Range, dan Guard #
match jauh lebih fleksibel dari switch di bahasa lain — satu arm bisa menangani beberapa pattern sekaligus, range nilai, dan kondisi tambahan:
fn main() {
let kode_http = 404;
let pesan = match kode_http {
// Pattern tunggal
200 => "OK",
201 => "Created",
// Multiple pattern dengan |
301 | 302 => "Redirect",
// Range inklusif
400..=499 => "Client Error",
500..=599 => "Server Error",
// Wildcard
_ => "Unknown",
};
println!("HTTP {}: {}", kode_http, pesan);
// Guard condition — filter tambahan setelah pattern
let bilangan = -5i32;
let kategori = match bilangan {
n if n < 0 => "negatif",
0 => "nol",
n if n % 2 == 0 => "genap positif",
_ => "ganjil positif",
};
println!("{} adalah {}", bilangan, kategori);
}
Binding dengan @
#
Operator @ memungkinkan kamu menangkap nilai yang cocok dengan pattern sekaligus memberinya nama untuk digunakan dalam arm body:
fn main() {
let usia = 17;
let keterangan = match usia {
// Tangkap nilai yang cocok dengan range ke dalam `n`
n @ 0..=12 => format!("Anak-anak, usia {}", n),
n @ 13..=17 => format!("Remaja, usia {}", n),
n @ 18..=64 => format!("Dewasa, usia {}", n),
n => format!("Lansia, usia {}", n),
};
println!("{}", keterangan);
// Tanpa @, kamu harus ulangi kondisi di dalam arm
// ANTI-PATTERN:
let keterangan2 = match usia {
13..=17 => format!("Remaja, usia {}", usia), // harus sebut usia lagi
_ => String::from("Lainnya"),
};
println!("{}", keterangan2);
}
match dengan Enum
#
match dan enum bekerja bersama sangat erat di Rust — ini adalah pola yang paling sering kamu temui di kode Rust idiomatis.
#[derive(Debug)]
enum Arah {
Utara,
Selatan,
Timur,
Barat,
}
#[derive(Debug)]
enum Perintah {
Gerak(Arah),
Berhenti,
Percepat { kecepatan: u32 },
Putar(f64), // derajat
}
fn proses(perintah: &Perintah) {
match perintah {
// Variant tanpa data
Perintah::Berhenti => println!("Berhenti"),
// Variant dengan tuple data — destructuring
Perintah::Gerak(arah) => println!("Bergerak ke {:?}", arah),
Perintah::Putar(derajat) => println!("Putar {} derajat", derajat),
// Variant dengan named fields — destructuring
Perintah::Percepat { kecepatan } => println!("Percepat ke {} km/h", kecepatan),
}
}
fn main() {
let perintah_list = vec![
Perintah::Gerak(Arah::Utara),
Perintah::Percepat { kecepatan: 60 },
Perintah::Putar(90.0),
Perintah::Berhenti,
];
for p in &perintah_list {
proses(p);
}
}
match dengan Option dan Result
#
Dua enum paling sering di-match adalah Option<T> dan Result<T, E>:
fn bagi(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
fn parse_angka(s: &str) -> Result<i32, std::num::ParseIntError> {
s.trim().parse()
}
fn main() {
// match Option
match bagi(10.0, 3.0) {
Some(hasil) => println!("Hasil: {:.4}", hasil),
None => println!("Tidak bisa dibagi nol"),
}
// match Result
match parse_angka("42") {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Error: {}", e),
}
// Nested match — menangani Option<Result<...>>
let input = Some("123");
match input {
Some(s) => match parse_angka(s) {
Ok(n) => println!("Angka valid: {}", n),
Err(_) => println!("Bukan angka valid"),
},
None => println!("Tidak ada input"),
}
}
Destructuring Tuple dan Struct dalam match
#
struct Titik {
x: i32,
y: i32,
}
fn main() {
// Destructuring tuple
let koordinat = (3, -5);
let kuadran = match koordinat {
(0, 0) => "Titik asal",
(x, 0) if x > 0 => "Sumbu X positif",
(0, y) if y > 0 => "Sumbu Y positif",
(x, y) if x > 0 && y > 0 => "Kuadran I",
(x, y) if x < 0 && y > 0 => "Kuadran II",
(x, y) if x < 0 && y < 0 => "Kuadran III",
_ => "Kuadran IV",
};
println!("{:?} → {}", koordinat, kuadran);
// Destructuring struct
let titik = Titik { x: 0, y: 7 };
match titik {
Titik { x: 0, y } => println!("Di sumbu Y, y = {}", y),
Titik { x, y: 0 } => println!("Di sumbu X, x = {}", x),
Titik { x, y } => println!("Titik lain: ({}, {})", x, y),
}
}
if let — Match Satu Pattern
#
if let adalah cara ringkas untuk menangani satu pattern dari match tanpa perlu menuliskan semua kemungkinan lain. Cocok ketika kamu hanya peduli pada satu kasus dan ingin mengabaikan yang lain.
fn main() {
let angka: Option<i32> = Some(42);
// ANTI-PATTERN: match berlebihan hanya untuk satu case yang dipedulikan
match angka {
Some(n) => println!("Ada angka: {}", n),
None => {}, // tidak ada yang dilakukan — kenapa ditulis?
}
// BENAR: if let lebih ringkas untuk kasus ini
if let Some(n) = angka {
println!("Ada angka: {}", n);
}
// if let dengan else
if let Some(n) = angka {
println!("Nilai: {}", n);
} else {
println!("Tidak ada nilai");
}
// if let bersarang untuk tipe kompleks
let data: Result<Option<i32>, &str> = Ok(Some(100));
if let Ok(Some(nilai)) = data {
println!("Berhasil dengan nilai: {}", nilai);
}
// if let dengan enum kustom
#[derive(Debug)]
enum Status { Aktif(String), Nonaktif }
let status = Status::Aktif(String::from("premium"));
if let Status::Aktif(tipe) = &status {
println!("Status aktif: {}", tipe);
}
}
Berantai if let / else if let
#
fn main() {
let config: Option<&str> = Some("debug");
if let Some("debug") = config {
println!("Mode debug aktif");
} else if let Some("release") = config {
println!("Mode release");
} else if let Some(mode) = config {
println!("Mode tidak dikenal: {}", mode);
} else {
println!("Tidak ada konfigurasi");
}
}
while let — Loop dengan Pattern
#
while let mengulangi loop selama pattern cocok. Paling sering digunakan untuk memproses koleksi yang menghasilkan Option hingga habis:
fn main() {
// Memproses stack sampai kosong
let mut tumpukan = vec![1, 2, 3, 4, 5];
while let Some(atas) = tumpukan.pop() {
println!("Diambil: {}", atas);
}
println!("Tumpukan kosong");
// while let dengan iterator manual
let data = vec!["apel", "mangga", "jeruk"];
let mut iter = data.iter();
while let Some(buah) = iter.next() {
println!("Buah: {}", buah);
}
// ANTI-PATTERN: loop dengan match eksplisit untuk kasus yang cocok with while let
let mut v = vec![10, 20, 30];
loop {
match v.pop() {
Some(n) => println!("{}", n),
None => break,
}
}
// BENAR: ekuivalen tapi lebih ringkas
let mut v = vec![10, 20, 30];
while let Some(n) = v.pop() {
println!("{}", n);
}
}
let-else — Destructuring dengan Fallback
#
Sejak Rust 1.65, ada konstruk baru: let-else. Ini memungkinkan kamu melakukan destructuring pada let biasa, tapi dengan blok else yang dijalankan jika pattern tidak cocok. Blok else harus selalu diverge — biasanya dengan return, break, continue, atau panic!.
fn proses_input(input: &str) -> Option<u32> {
// ANTI-PATTERN: if let bersarang yang dalam
if let Ok(angka) = input.trim().parse::<u32>() {
if angka > 0 {
println!("Input valid: {}", angka);
return Some(angka);
}
}
None
// BENAR: let-else memperatar alur kode — happy path tidak bersarang
}
fn proses_dengan_let_else(input: &str) -> Option<u32> {
// Jika parse gagal, langsung return None — alur utama tetap datar
let Ok(angka) = input.trim().parse::<u32>() else {
return None;
};
// Jika angka nol, langsung return None
let angka = if angka > 0 {
angka
} else {
return None;
};
println!("Input valid: {}", angka);
Some(angka)
}
fn main() {
println!("{:?}", proses_dengan_let_else("42")); // Some(42)
println!("{:?}", proses_dengan_let_else("abc")); // None
println!("{:?}", proses_dengan_let_else("0")); // None
}
let-else sangat berguna untuk validasi input di awal fungsi — setiap kondisi invalid langsung di-reject di baris pertama tanpa membuat alur utama bersarang:
struct Pengguna {
nama: String,
usia: u8,
}
fn buat_pengguna(nama: &str, usia_str: &str) -> Option<Pengguna> {
// Validasi berurutan dengan let-else — alur tetap datar
let nama = nama.trim();
let Ok(usia) = usia_str.trim().parse::<u8>() else {
eprintln!("Usia tidak valid: {}", usia_str);
return None;
};
let (18..=120) = usia else { // versi unstable, tapi konsepnya sama
eprintln!("Usia harus antara 18-120");
return None;
};
Some(Pengguna {
nama: nama.to_string(),
usia,
})
}
Kapan Memilih if vs match
#
Kedua konstruksi ini sering bisa digunakan secara bergantian, tapi ada situasi di mana satu jauh lebih tepat dari yang lain:
flowchart TD
Q{Apa yang dibandingkan?}
Q --> A{Kondisi boolean\natau range numerik\nyang kompleks?}
Q --> B{Nilai dari enum\natau beberapa\nnilai diskrit?}
Q --> C{Satu pattern\ndari Option/Result?}
A -- Ya --> D[Gunakan if / else if\nLebih natural untuk\nkondisi boolean kompleks]
B -- Ya --> E[Gunakan match\nExhaustive, lebih aman\nbisa destructuring]
C -- Ya --> F[Gunakan if let\nLebih ringkas dari\nmatch untuk satu case]
E --> G{Pattern sangat\nbanyak dan bertingkat?}
G -- Ya --> H[Pertimbangkan refactor\nke beberapa fungsi kecil]
G -- Tidak --> I[match langsung sudah tepat]fn main() {
let nilai = 85;
let status: Option<String> = Some(String::from("aktif"));
let hasil: Result<i32, &str> = Ok(42);
// Kondisi numerik dengan logika kompleks → if lebih natural
if nilai >= 90 && nilai <= 100 {
println!("Sempurna");
} else if nilai >= 75 {
println!("Lulus");
} else {
println!("Gagal");
}
// Banyak nilai diskrit dari enum/integer → match lebih baik
let grade = match nilai {
90..=100 => 'A',
80..=89 => 'B',
70..=79 => 'C',
60..=69 => 'D',
_ => 'E',
};
println!("Grade: {}", grade);
// Satu case dari Option → if let paling ringkas
if let Some(s) = &status {
println!("Status: {}", s);
}
// Semua case dari Result → match untuk kelengkapan
match hasil {
Ok(n) => println!("Ok: {}", n),
Err(e) => println!("Err: {}", e),
}
}
Ringkasan #
ifdanmatchadalah expression — keduanya menghasilkan nilai yang bisa diassign ke variabel. Tidak perlu operator ternary?:seperti di bahasa lain.- Kondisi
ifharus bertipeboolsecara eksak — tidak ada konversi implisit dari integer atau pointer.if angkabukan kode Rust yang valid.matchbersifat exhaustive — compiler menolak kode yang tidak menangani semua kemungkinan. Gunakan_sebagai wildcard untuk menangkap case yang tidak relevan.- Semua arm
matchharus bertipe sama — jikamatchdigunakan sebagai expression, setiap arm harus menghasilkan tipe yang sama persis.- Pattern
matchmendukung nilai tunggal, multiple pattern dengan|, range dengan..=, guard denganif, binding dengan@, dan destructuring tuple/struct/enum.if letuntuk satu pattern — lebih ringkas darimatchketika kamu hanya peduli pada satu case dan ingin mengabaikan yang lain.while letuntuk loop berbasis pattern — paling idiomatis untuk memproses stack atau iterator yang menghasilkanOptionhingga habis.let-elseuntuk validasi awal — memungkinkan destructuring di barisletdengan fallback yang langsung keluar dari fungsi, menjaga alur utama tetap datar tanpa nesting.- Gunakan
matchuntuk enum — exhaustiveness checking memastikan kamu tidak lupa menangani variant baru yang ditambahkan ke enum.