Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Konstruk Control Flow match

Rust punya konstruk control flow yang sangat kuat (powerful) namanya match yang ngebolehin kita bandingin sebuah nilai sama serangkaian pattern (pola) terus ngejalanin kode berdasarkan pattern mana yang cocok. Pattern bisa dibuat dari nilai literal, nama variabel, wildcards, dan banyak hal lainnya; Bab 19 ngebahas semua jenis pattern yang beda dan apa fungsinya. Kekuatan dari match dateng dari ekspresifitas pattern-nya dan fakta kalau compiler mastiin semua kemungkinan kasus udah di-handle.

Bayangin ekspresi match itu kayak mesin penyortir koin: koin meluncur turun di jalur yang punya lubang dengan berbagai ukuran, dan tiap koin bakal jatuh ke lubang pertama yang pas buat dia. Dengan cara yang sama, nilai bakal ngelewatin tiap pattern di sebuah match, dan di pattern pertama di mana nilainya “pas,” nilai itu bakal masuk ke blok kode terkait buat dipake pas eksekusi.

Ngomong-ngomong soal koin, yuk kita pake mereka sebagai contoh buat match! Kita bisa nulis fungsi yang nerima koin US yang nggak tau jenisnya apa dan, dengan cara yang mirip kayak mesin penghitung, nentuin koin apa itu terus balikin nilainya dalam satuan sen (cents), kayak yang ditunjukin di Listing 6-3.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}
Listing 6-3: Sebuah enum dan ekspresi match yang punya varian enum sebagai pattern-nya

Yuk kita bedah ekspresi match di fungsi value_in_cents. Pertama kita nulis keyword match diikuti sama sebuah ekspresi, yang di kasus ini adalah nilai coin. Ini keliatannya mirip sekali sama ekspresi kondisional yang dipake bareng if, tapi ada perbedaan gede: kalau pake if, kondisinya harus dievaluasi jadi nilai Boolean, tapi di sini dia bisa jadi tipe apa pun. Tipe dari coin di contoh ini adalah enum Coin yang kita definisikan di baris pertama.

Selanjutnya adalah arms (lengan) dari match. Sebuah arm punya dua bagian: sebuah pattern dan sejumlah kode. Arm pertama di sini punya pattern yaitu nilai Coin::Penny dan terus operator => yang misahin pattern sama kode yang bakal dijalanin. Kode di kasus ini cuma nilai 1. Tiap arm dipisahin dari arm berikutnya pake tanda koma.

Pas ekspresi match jalan, dia bandingin nilai hasilnya sama pattern dari tiap arm, secara berurutan. Kalau ada pattern yang cocok sama nilainya, kode yang terkait sama pattern itu bakal dijalanin. Kalau pattern itu nggak cocok sama nilainya, eksekusi bakal lanjut ke arm berikutnya, sama persis kayak di mesin penyortir koin. Kita bisa punya sebanyak apa pun arm yang kita butuhin: di Listing 6-3, match kita punya empat arm.

Kode yang terkait sama tiap arm itu adalah sebuah ekspresi, dan nilai hasil dari ekspresi di arm yang cocok adalah nilai yang bakal dibalikin buat seluruh ekspresi match-nya.

Kita biasanya nggak pake kurung kurawal kalau kode match arm-nya pendek, kayak di Listing 6-3 di mana tiap arm cuma balikin sebuah nilai. Kalau kita mau ngejalanin beberapa baris kode di sebuah match arm, kita wajib pake kurung kurawal, dan koma setelah arm-nya itu jadi opsional. Misalnya, kode berikut nyetak “Lucky penny!” tiap kali method-nya dipanggil pake Coin::Penny, tapi tetep balikin nilai terakhir dari blok-nya, yaitu 1:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {}

Pattern yang Nge-bind ke Nilai

Fitur berguna lainnya dari match arms adalah mereka bisa nge-bind ke bagian- bagian nilai yang cocok sama pattern-nya. Ini cara kita ngekstrak nilai keluar dari varian enum.

Sebagai contoh, yuk kita ubah salah satu varian enum kita biar nyimpen data di dalemnya. Dari tahun 1999 sampe 2008, Amerika Serikat nyetak koin quarter (25 sen) dengan desain beda-beda buat tiap 50 negara bagian di satu sisinya. Nggak ada koin lain yang dapet desain negara bagian, jadi cuma quarter yang punya nilai ekstra ini. Kita bisa nambahin informasi ini ke enum kita dengan ngerubah varian Quarter biar masukin nilai UsState yang disimpan di dalemnya, kayak yang kita lakuin di Listing 6-4.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {}
Listing 6-4: Sebuah enum Coin di mana varian Quarter juga nyimpen nilai UsState

Bayangin ada temen kita yang lagi nyoba ngumpulin semua 50 state quarters. Pas kita lagi nyortir uang receh berdasarkan jenis koinnya, kita juga bakal nyebutin nama negara bagian yang terkait sama tiap quarter biar kalau temen kita belum punya yang itu, dia bisa nambahin ke koleksinya.

Di ekspresi match buat kode ini, kita nambahin variabel namanya state ke pattern yang nyocokin nilai varian Coin::Quarter. Pas sebuah Coin::Quarter cocok, variabel state bakal di-bind ke nilai negara bagian dari quarter itu. Terus kita bisa pake state di kode buat arm itu, kayak gini:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {state:?}!");
            25
        }
    }
}

fn main() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

Kalau kita manggil value_in_cents(Coin::Quarter(UsState::Alaska)), coin bakal jadi Coin::Quarter(UsState::Alaska). Pas kita bandingin nilai itu sama tiap match arms, nggak ada satu pun yang cocok sampe kita nyampe Coin::Quarter(state). Di titik itu, binding buat state bakal jadi nilai UsState::Alaska. Kita terus bisa pake binding itu di ekspresi println!, dan dengan gitu kita dapet nilai state dalemnya keluar dari varian enum Coin buat Quarter.

Matching dengan Option<T>

Di bagian sebelumnya, kita mau dapet nilai T di dalem dari kasus Some pas lagi pake Option<T>; kita juga bisa nanganin Option<T> pake match, sama kayak yang kita lakuin sama enum Coin! Bukannya ngebandingin koin, kita bakal ngebandingin varian Option<T>, tapi cara kerja ekspresi match-nya tetep sama.

Katakanlah kita mau nulis fungsi yang nerima sebuah Option<i32> dan, kalau ada nilai di dalemnya, nambahin 1 ke nilai itu. Kalau nggak ada nilainya, fungsi itu harus balikin nilai None dan nggak nyoba ngelakuin operasi apa pun.

Fungsi ini gampang sekali ditulis, berkat match, dan bakal keliatan kayak Listing 6-5.

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}
Listing 6-5: Fungsi yang pake ekspresi match pada sebuah Option<i32>

Yuk kita teliti eksekusi pertama dari plus_one lebih dalem. Pas kita manggil plus_one(five), variabel x di body plus_one bakal punya nilai Some(5). Kita terus bandingin itu sama tiap match arm:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Nilai Some(5) nggak cocok sama pattern None, jadi kita lanjut ke arm berikutnya:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Apakah Some(5) cocok sama Some(i)? Cocok dong! Kita dapet varian yang sama. i di-bind ke nilai yang ditampung di Some, jadi i ngambil nilai 5. Kode di match arm itu kemudian dijalanin, jadi kita nambahin 1 ke nilai i dan bikin nilai Some baru dengan total 6 kita di dalemnya.

Sekarang yuk kita pertimbangkan pemanggilan kedua dari plus_one di Listing 6-5, di mana x itu None. Kita masuk ke match dan bandingin sama arm pertama:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Cocok! Nggak ada nilai buat ditambahin, jadi programnya berhenti dan balikin nilai None di sisi kanan =>. Karena arm pertama cocok, nggak ada arm lain yang dibandingin.

Gabungin match sama enum itu berguna di banyak situasi. Kita bakal sering liat pola ini di kode Rust: nge-match terhadap sebuah enum, nge-bind variabel ke data di dalemnya, terus ngejalanin kode berdasarkan data itu. Agak ribet di awal emang, tapi sekali kita udah terbiasa, kita bakal ngarep ini ada di semua bahasa. Ini terus-terusan jadi favorit user.

Matches Itu Exhaustive (Menyeluruh)

Ada satu aspek lain dari match yang perlu kita bahas: pattern-pattern di arm-nya harus mencakup semua kemungkinan. Coba liat versi dari fungsi plus_one kita ini, yang punya bug dan nggak bakal bisa di-compile:

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Kita nggak nge-handle kasus None, jadi kode ini bakal nyebabin bug. Untungnya, ini adalah bug yang Rust tau cara nangkepnya. Kalau kita nyoba compile kode ini, kita bakal dapet error kayak gini:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:3:15
  |
3 |         match x {
  |               ^ pattern `None` not covered
  |
note: `Option<i32>` defined here
 --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
 ::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
  |
  = note: not covered
  = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
4 ~             Some(i) => Some(i + 1),
5 ~             None => todo!(),
  |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error

Rust tau kalau kita nggak mencakup semua kasus yang mungkin, dan bahkan tau pattern mana yang kita kelupaan! Matches di Rust itu exhaustive: kita harus ngabisin setiap kemungkinan yang ada biar kodenya jadi valid. Terutama buat kasus Option<T>, pas Rust nyegah kita dari lupa buat secara eksplisit nanganin kasus None, dia ngelindungin kita dari ngasumsikan kalau kita punya nilai padahal mungkin kita punya null, makanya ini ngebikin kesalahan satu miliar dolar yang dibahas sebelumnya jadi mustahil terjadi.

Catch-All Patterns dan Placeholder _

Pake enum, kita juga bisa ngambil aksi khusus buat beberapa nilai tertentu, tapi buat semua nilai lainnya kita mau ngambil satu aksi default. Bayangin kita lagi bikin game di mana kalau kita ngelempar dadu dan dapet 3, player kita nggak gerak, tapi malah dapet topi fancy baru. Kalau dapet 7, player kita kehilangan topi fancy-nya. Buat semua nilai lainnya, player kita maju sejumlah langkah sesuai angka itu di papan game. Ini ekspresi match yang mengimplementasikan logika itu, dengan hasil lemparan dadu yang di-hardcoded bukannya nilai random, dan semua logika lainnya direpresentasikan sama fungsi tanpa body karena implementasi aslinya di luar cakupan contoh ini:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        other => move_player(other),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn move_player(num_spaces: u8) {}
}

Buat dua arm pertama, pattern-nya adalah nilai literal 3 dan 7. Buat arm terakhir yang nyakup semua kemungkinan nilai lainnya, pattern-nya adalah variabel yang kita pilih buat dikasih nama other. Kode yang jalan buat arm other pake variabel itu dengan masukin dia ke fungsi move_player.

Kode ini berhasil di-compile, walaupun kita belum nyebutin semua nilai yang mungkin dipunyai u8, karena pattern terakhir bakal nyocokin semua nilai yang nggak disebutin secara spesifik. Catch-all pattern ini menuhin syarat kalau match itu harus exhaustive. Perhatiin ya kalau kita harus naruh arm catch-all di paling akhir karena pattern-nya dievaluasi berurutan. Kalau kita naruh arm catch-all lebih awal, arm lainnya nggak bakal pernah jalan, jadi Rust bakal ngasih tau kita kalau kita nambahin arm setelah catch-all!

Rust juga punya pattern yang bisa kita pake pas kita butuh catch-all tapi nggak mau pake nilainya di pattern catch-all itu: _ adalah pattern khusus yang nyocokin nilai apa pun dan nggak nge-bind ke nilai itu. Ini ngasih tau Rust kalau kita nggak bakal pake nilainya, jadi Rust nggak bakal ngasih warning soal variabel yang nggak kepake.

Yuk kita ubah aturan gamenya: sekarang, kalau kita ngelempar dadu selain angka 3 atau 7, kita harus ngelempar lagi. Kita udah nggak perlu pake nilai catch-all, jadi kita bisa ubah kode kita pake _ bukannya variabel namanya other:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => reroll(),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn reroll() {}
}

Contoh ini juga menuhin syarat exhaustiveness karena secara eksplisit kita ngabaikan semua nilai lain di arm terakhir; kita nggak ada yang kelupaan.

Terakhir, kita bakal ubah aturan gamenya sekali lagi biar nggak ada yang terjadi pas giliran kita kalau kita ngelempar selain angka 3 atau 7. Kita bisa ekspresikan itu pake nilai unit (tipe tuple kosong yang pernah kita sebut di bagian “Tipe Tuple”) sebagai kode yang nyertain arm _:

fn main() {
    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
}

Di sini, kita ngasih tau Rust secara eksplisit kalau kita nggak bakal pake nilai lain apa pun yang nggak cocok sama pattern di arm sebelumnya, dan kita nggak mau ngejalanin kode apa pun di kasus ini.

Masih banyak hal lain soal pattern dan matching yang bakal kita bahas di Bab 19. Buat sekarang, kita bakal lanjut ke sintaks if let, yang bisa kepake di situasi-situasi di mana ekspresi match dirasa agak kepanjangan (wordy).