Seleksi Kondisi

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 #

  • if dan match adalah expression — keduanya menghasilkan nilai yang bisa diassign ke variabel. Tidak perlu operator ternary ?: seperti di bahasa lain.
  • Kondisi if harus bertipe bool secara eksak — tidak ada konversi implisit dari integer atau pointer. if angka bukan kode Rust yang valid.
  • match bersifat exhaustive — compiler menolak kode yang tidak menangani semua kemungkinan. Gunakan _ sebagai wildcard untuk menangkap case yang tidak relevan.
  • Semua arm match harus bertipe sama — jika match digunakan sebagai expression, setiap arm harus menghasilkan tipe yang sama persis.
  • Pattern match mendukung nilai tunggal, multiple pattern dengan |, range dengan ..=, guard dengan if, binding dengan @, dan destructuring tuple/struct/enum.
  • if let untuk satu pattern — lebih ringkas dari match ketika kamu hanya peduli pada satu case dan ingin mengabaikan yang lain.
  • while let untuk loop berbasis pattern — paling idiomatis untuk memproses stack atau iterator yang menghasilkan Option hingga habis.
  • let-else untuk validasi awal — memungkinkan destructuring di baris let dengan fallback yang langsung keluar dari fungsi, menjaga alur utama tetap datar tanpa nesting.
  • Gunakan match untuk enum — exhaustiveness checking memastikan kamu tidak lupa menangani variant baru yang ditambahkan ke enum.

← Sebelumnya: Operator   Berikutnya: Perulangan →

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