Lewati ke konten

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.

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 beku
0°C terasa tepat nol
18°C terasa nyaman
35°C terasa panas
▶ Coba sendiri

Rust mengecek cabang dari atas ke bawah, jadi urutannya penting. 18 gagal di guard pertama, bukan 0, dan lolos t < 25, jadi hasilnya “nyaman”.

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 = 5
▶ Coba sendiri

Perhatikan 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 }.

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.57
luas = 13.50
luas = 2.25
▶ Coba sendiri

Kamu 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 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 2
berkata: halo
sampai jumpa
▶ Coba sendiri

Tanpa @, rentang seperti 1..=9 memang mengecek nilainya, tapi lalu membuangnya. Dengan @, kamu bisa menyimpannya.

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 = 255
pertama = 1, terakhir = 5
Mira ada di level 7
▶ Coba sendiri

.. hanya boleh muncul sekali dalam satu pola. (first, .., last) boleh, tapi (.., middle, ..) tidak, karena Rust tidak bisa tahu nilai mana yang kamu maksud.

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:

1920x1080
x: 4, y: -2
[3, 7, 11]
▶ Coba sendiri

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.

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:

true
3 lulus
Some(31)
'tiga puluh' bukan angka
None
▶ Coba sendiri

let-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.

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 exhaustivity

Pesan 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 value
6 | _ => println!("Salah"),
| ^ no value can reach this

Solusinya adalah guard: g if g == expected => println!("Benar!").

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:

Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Coba sendiri

(0, 0) harus ada paling atas. Kalau (0, _) yang lebih dulu, 15 akan ditampilkan sebagai Fizz.

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 500
saldo = 115
▶ Coba sendiri

Cabang 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.

  • 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 @ pola menguji sebuah nilai sekaligus menyimpannya di variabel.
  • _ melewati satu nilai; .. melewati sisanya. Pola juga bisa dipakai di let, parameter fungsi, dan closure.
  • matches! memberi jawaban cepat true/false; let-else membongkar sebuah nilai atau keluar lebih awal.

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