Pattern Matching di Rust
Di halaman Match, kamu sudah belajar dasarnya: cabang (arm), _, mengelompokkan nilai dengan |, dan rentang. Itu baru permulaan. Pattern (pola) di Rust juga bisa melihat ke dalam sebuah nilai, mengambil bagian yang kamu butuhkan, dan mengecek kondisi tambahan, semuanya dalam satu baris. Inilah yang disebut pattern matching (pencocokan pola).
Bayangkan pola seperti cetakan kue. Kamu menekannya ke sebuah nilai. Kalau bentuknya pas, Rust memberikan potongan-potongan di dalamnya kepadamu. Kalau tidak pas, Rust mencoba cabang berikutnya. Begitu kamu paham konsep ini, kamu akan memakai pola di mana-mana: di match, di let, di parameter fungsi, dan di if let.
Match guard: menambahkan if
Section titled “Match guard: menambahkan if”Kadang bentuknya saja tidak cukup, dan kamu butuh pengecekan tambahan. Match guard adalah kondisi if yang ditulis setelah pola. Cabangnya hanya dijalankan kalau polanya pas dan kondisinya bernilai true.
fn main() { let temperatures = [-5, 0, 18, 35];
for t in temperatures { let feeling = match t { t if t < 0 => "beku", 0 => "tepat nol", t if t < 25 => "nyaman", _ => "panas", }; println!("{t}°C terasa {feeling}"); }}Output:
-5°C terasa beku0°C terasa tepat nol18°C terasa nyaman35°C terasa panasRust mengecek cabang dari atas ke bawah, jadi urutannya penting. 18 gagal di guard pertama, bukan 0, dan lolos t < 25, jadi hasilnya “nyaman”.
Destructuring tuple dan struct
Section titled “Destructuring tuple dan struct”Destructuring (membongkar) artinya memecah sebuah nilai menjadi bagian-bagiannya. Untuk tuple, tulis polanya dengan bentuk yang sama seperti tuple-nya. Untuk struct, sebutkan nama field yang kamu inginkan.
struct Point { x: i32, y: i32,}
fn main() { let pair = (3, -7); match pair { (0, 0) => println!("titik asal"), (x, 0) => println!("di sumbu x pada {x}"), (0, y) => println!("di sumbu y pada {y}"), (x, y) => println!("di tempat lain: ({x}, {y})"), }
let p = Point { x: 0, y: 5 }; match p { Point { x: 0, y } => println!("di sumbu y, y = {y}"), Point { x, y: 0 } => println!("di sumbu x, x = {x}"), Point { x, y } => println!("di ({x}, {y})"), }}Output:
di tempat lain: (3, -7)di sumbu y, y = 5Perhatikan campurannya. 0 adalah nilai tetap yang harus cocok persis. x dan y adalah variabel baru yang menangkap nilai apa pun yang ada di sana. Point { x, y } adalah singkatan dari Point { x: x, y: y }.
Destructuring enum yang membawa data
Section titled “Destructuring enum yang membawa data”Di sinilah pola benar-benar bersinar. Sebuah varian enum bisa membawa data, dan pola bisa membongkar data itu sambil mengecek varian mana yang kamu punya.
enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Square(f64),}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Square(side) => side * side, }}
fn main() { let shapes = [ Shape::Circle { radius: 2.0 }, Shape::Rectangle { width: 3.0, height: 4.5 }, Shape::Square(1.5), ]; for shape in &shapes { println!("luas = {:.2}", area(shape)); }}Output:
luas = 12.57luas = 13.50luas = 2.25Kamu tidak bisa mengakses radius tanpa lebih dulu membuktikan bahwa nilainya adalah Circle. Itulah keuntungan dari sisi keamanan: compiler tidak akan membiarkanmu membaca data dari varian yang salah.
Pola bersarang dan binding @
Section titled “Pola bersarang dan binding @”Pola bisa ditaruh di dalam pola lain, sedalam apa pun yang kamu mau. Di sini, enum Command menyimpan enum Direction, dan satu cabang mencocokkan kedua tingkat itu sekaligus.
Binding @ membuatmu bisa menguji sebuah nilai dengan sebuah pola sekaligus menyimpannya di variabel. n @ 1..=9 artinya “cek bahwa nilainya antara 1 dan 9, lalu beri nama n”.
enum Direction { Up, Down,}
enum Command { Move(Direction, u32), Say(String), Quit,}
fn run(cmd: Command) { match cmd { Command::Move(Direction::Up, steps @ 1..=9) => println!("naik {steps} (langkah kecil)"), Command::Move(Direction::Up, steps) => println!("naik {steps} (lompatan besar!)"), Command::Move(Direction::Down, steps) => println!("turun {steps}"), Command::Say(text) => println!("berkata: {text}"), Command::Quit => println!("sampai jumpa"), }}
fn main() { run(Command::Move(Direction::Up, 3)); run(Command::Move(Direction::Up, 50)); run(Command::Move(Direction::Down, 2)); run(Command::Say(String::from("halo"))); run(Command::Quit);}Output:
naik 3 (langkah kecil)naik 50 (lompatan besar!)turun 2berkata: halosampai jumpaTanpa @, rentang seperti 1..=9 memang mengecek nilainya, tapi lalu membuangnya. Dengan @, kamu bisa menyimpannya.
Mengabaikan bagian dengan _ dan ..
Section titled “Mengabaikan bagian dengan _ dan ..”Sering kali kamu hanya peduli dengan sebagian isinya. Pakai _ untuk melewati satu nilai. Pakai .. untuk melewati “semua sisanya”.
struct Player { name: String, level: u32, gold: u32,}
fn main() { let rgb = (255, 128, 0); let (red, _, _) = rgb; println!("merah = {red}");
let numbers = (1, 2, 3, 4, 5); let (first, .., last) = numbers; println!("pertama = {first}, terakhir = {last}");
let hero = Player { name: String::from("Mira"), level: 7, gold: 120 }; let Player { name, level, .. } = hero; println!("{name} ada di level {level}");}Output:
merah = 255pertama = 1, terakhir = 5Mira ada di level 7.. hanya boleh muncul sekali dalam satu pola. (first, .., last) boleh, tapi (.., middle, ..) tidak, karena Rust tidak bisa tahu nilai mana yang kamu maksud.
Pola di let dan parameter fungsi
Section titled “Pola di let dan parameter fungsi”Setiap let sebenarnya adalah sebuah pola. Begitu juga setiap parameter fungsi. Artinya, kamu bisa melakukan destructuring tepat di tempat nilainya datang.
fn print_coords(&(x, y): &(i32, i32)) { println!("x: {x}, y: {y}");}
fn main() { let (width, height) = (1920, 1080); println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)]; let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect(); println!("{:?}", sums);}Output:
1920x1080x: 4, y: -2[3, 7, 11]Closure |&(a, b)| a + b memakai ide yang sama. Setiap item bertipe &(i32, i32), dan polanya langsung membongkarnya menjadi a dan b. Kamu akan sering melihat ini saat memakai iterator.
matches! dan let-else
Section titled “matches! dan let-else”Ada dua alat praktis yang menghemat kamu dari menulis match lengkap.
matches!(value, pattern) mengembalikan true atau false. Sangat cocok dipakai di dalam if atau .filter() saat kamu hanya butuh jawaban ya/tidak. Guard juga bisa dipakai di sini.
let-else menangani situasi “bongkar ini, atau keluar sekarang”. Tulis let POLA = nilai else { ... };. Kalau polanya pas, variabel-variabelnya bisa dipakai di sisa fungsi. Kalau tidak, blok else dijalankan, dan blok itu harus keluar, dengan return, break, continue, atau panic.
fn parse_age(input: &str) -> Option<u32> { let Ok(age) = input.parse::<u32>() else { println!("'{input}' bukan angka"); return None; }; Some(age)}
fn main() { let grade = 'B'; println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66]; let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count(); println!("{passed} lulus");
println!("{:?}", parse_age("31")); println!("{:?}", parse_age("tiga puluh"));}Output:
true3 lulusSome(31)'tiga puluh' bukan angkaNonelet-else menjaga “jalur bahagia” (happy path) tetap datar. Alih-alih membungkus kode kamu di dalam if let ... { }, kamu tangani dulu kasus yang buruk, lalu lanjut. Pasangkan dengan nilai Option dan Result.
Kesalahan umum
Section titled “Kesalahan umum”Guard tidak dihitung untuk “semua kemungkinan sudah ditangani”. Compiler tidak bisa menalar kondisi if milikmu, jadi kode ini gagal meskipun kedua guard sebenarnya mencakup semua angka:
fn main() { let n: i32 = 7; match n { x if x < 0 => println!("negatif"), x if x >= 0 => println!("nol atau positif"), }}error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered --> src/main.rs:3:11 |3 | match n { | ^ pattern `i32::MIN..=i32::MAX` not covered | = note: the matched value is of type `i32` = note: match arms with guards don't count towards exhaustivityPesan error ini memberi tahu bahwa polanya tidak lengkap (non-exhaustive): rentang i32::MIN..=i32::MAX belum tertangani, karena cabang yang punya guard tidak dihitung. Solusinya: jadikan cabang terakhir sebagai penampung biasa, misalnya _ => println!("nol atau positif").
Nama variabel di dalam pola adalah variabel baru, bukan perbandingan. Kode ini terlihat seperti mengecek guess == expected, padahal tidak:
fn main() { let expected = 3; let guess = 5; match guess { expected => println!("Benar!"), _ => println!("Salah"), }}Kode ini bisa dikompilasi dan menampilkan Benar!, karena expected di cabang itu hanya membuat variabel baru yang menangkap nilai apa pun. Rust memperingatkanmu bahwa pola _ tidak akan pernah tercapai (unreachable pattern):
warning: unreachable pattern --> src/main.rs:6:9 |5 | expected => println!("Benar!"), | -------- matches any value6 | _ => println!("Salah"), | ^ no value can reach thisSolusinya adalah guard: g if g == expected => println!("Benar!").
Latihan
Section titled “Latihan”1. FizzBuzz dengan tuple
Section titled “1. FizzBuzz dengan tuple”Tampilkan angka 9 sampai 15, tapi tampilkan Fizz untuk kelipatan 3, Buzz untuk kelipatan 5, dan FizzBuzz untuk kelipatan keduanya. Kerjakan dengan satu match pada sebuah tuple, bukan rantai if. Petunjuk: cocokkan (n % 3, n % 5). Sisa bagi 0 artinya “habis dibagi”, dan _ artinya “tidak peduli”.
Lihat solusi
fn main() { for n in 9..=15 { match (n % 3, n % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), _ => println!("{n}"), } }}Output:
FizzBuzz11Fizz1314FizzBuzz(0, 0) harus ada paling atas. Kalau (0, _) yang lebih dulu, 15 akan ditampilkan sebagai Fizz.
2. Transaksi bank
Section titled “2. Transaksi bank”Definisikan enum Transaction { Deposit(u32), Withdraw(u32), Fee }. Mulai dengan balance (saldo) 100, lalu terapkan daftar ini secara berurutan: setor 50, tarik 30, tarik 500, biaya. Biaya (fee) sebesar 5. Penarikan yang lebih besar dari saldo harus ditolak dengan sebuah pesan. Tampilkan saldo akhirnya. Petunjuk: pakai match guard, Transaction::Withdraw(amount) if amount <= balance, diikuti cabang Withdraw kedua untuk kasus yang ditolak.
Lihat solusi
enum Transaction { Deposit(u32), Withdraw(u32), Fee,}
fn main() { let mut balance: u32 = 100; let history = [ Transaction::Deposit(50), Transaction::Withdraw(30), Transaction::Withdraw(500), Transaction::Fee, ];
for t in history { match t { Transaction::Deposit(amount) => balance += amount, Transaction::Withdraw(amount) if amount <= balance => balance -= amount, Transaction::Withdraw(amount) => println!("ditolak: tidak bisa menarik {amount}"), Transaction::Fee => balance -= 5, } }
println!("saldo = {balance}");}Output:
ditolak: tidak bisa menarik 500saldo = 115Cabang Withdraw kedua tidak punya guard, jadi ia menangkap setiap penarikan yang ditolak oleh cabang pertama. Tanpa cabang itu, match-nya tidak akan bisa dikompilasi: guard tidak dihitung saat Rust mengecek apakah semua kemungkinan sudah ditangani.
Ringkasan
Section titled “Ringkasan”- Match guard (
pola if kondisi) menambahkan pengecekan ekstra pada sebuah cabang. - Pola bisa membongkar (destructure) tuple, struct, dan varian enum, dan bisa juga bersarang.
nama @ polamenguji sebuah nilai sekaligus menyimpannya di variabel._melewati satu nilai;..melewati sisanya. Pola juga bisa dipakai dilet, parameter fungsi, dan closure.matches!memberi jawaban cepattrue/false; let-else membongkar sebuah nilai atau keluar lebih awal.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Apa yang dilakukan n @ 1..=9 di sebuah cabang match?
2. Di let-else, apa yang harus dilakukan blok else?
3. Apa hasil dari let (first, .., last) = (1, 2, 3, 4, 5);?
4. Kenapa match yang semua cabangnya memakai guard sering gagal dikompilasi?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Patterns and Matching — The Rust Book (bahasa Inggris): bab resmi tentang semua tempat pola bisa muncul dan kapan pola bisa gagal cocok.
- Pattern Syntax — The Rust Book (bahasa Inggris): tur semua bentuk pola, dari rentang dan destructuring sampai guard dan binding
@. - Staying on the Happy Path with let…else — The Rust Book (bahasa Inggris): contoh let-else yang dibahas pelan-pelan.
- Destructuring — Rust by Example (bahasa Inggris): contoh destructuring tuple, array, enum, pointer, dan struct yang bisa dijalankan.
- Makro
matches!— dokumentasi std (bahasa Inggris): sintaks persis makro ini, termasuk cara menambahkan guard.