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

Sintaks Pattern

Di bagian ini, kita mengumpulkan semua sintaks yang valid buat dipakai di dalam patterns dan ngebahas kenapa dan kapan kita mungkin mau memakai masing-masing dari sintaks tersebut.

Mencocokkan Literals (Nilai Harfiah)

Seperti yang udah kita lihat di Bab 6, kita bisa mencocokkan patterns secara langsung dengan literals. Kode berikut ini ngasih beberapa contoh:

fn main() {
    let x = 1;

    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

Kode ini mencetak one karena nilai di dalam x adalah 1. Sintaks ini berguna pas kita pengen kode kita mengambil suatu tindakan tertentu kalau dia dapat sebuah nilai konkret yang spesifik.

Mencocokkan Variabel Bernama (Named Variables)

Variabel bernama adalah patterns irrefutable yang bakal cocok dengan nilai apa pun, dan kita udah memakainya berkali-kali di buku ini. Namun, ada sedikit kerumitan pas kita memakai variabel bernama di dalam ekspresi match, if let, atau while let. Karena setiap jenis ekspresi ini memulai sebuah scope (ruang lingkup) baru, variabel yang dideklarasikan sebagai bagian dari sebuah pattern di dalam ekspresi ini bakal menimpa (shadow) variabel dengan nama yang sama yang ada di luar konstruk tersebut, sama halnya kayak yang terjadi pada semua variabel di Rust. Di Listing 19-11, kita mendeklarasikan sebuah variabel bernama x dengan nilai Some(5) dan sebuah variabel y dengan nilai 10. Terus kita membikin ekspresi match pada nilai x. Coba perhatikan patterns di dalam match arms-nya dan println! di akhir, dan cobalah buat menebak apa yang bakal dicetak oleh kode ini sebelum menjalankannya atau membaca lebih lanjut.

Filename: src/main.rs
fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => println!("Matched, y = {y}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}
Listing 19-11: Sebuah ekspresi match dengan sebuah arm yang memperkenalkan variabel baru yang menimpa variabel y yang sudah ada

Mari kita telusuri apa yang terjadi pas ekspresi match ini dijalankan. Pattern di arm pertama tidak cocok dengan nilai yang udah didefinisikan buat x, jadi kodenya lanjut.

Pattern di arm kedua memperkenalkan sebuah variabel baru bernama y yang bakal cocok dengan nilai apa pun yang ada di dalam sebuah nilai Some. Karena kita sekarang ada di dalam scope baru di dalam ekspresi match ini, ini adalah variabel y yang baru, bukannya y yang kita deklarasikan di awal tadi yang nilainya 10. Binding y yang baru ini bakal cocok dengan nilai apa pun di dalam sebuah Some, yang mana itulah yang kita punya di dalam x. Oleh karena itu, si y baru ini mengikat (binds) dirinya ke nilai internal yang ada di dalam Some yang dimiliki x. Nilai itu adalah 5, jadi ekspresi buat arm tersebut dieksekusi dan mencetak Matched, y = 5.

Seandainya x tadi adalah sebuah nilai None bukannya Some(5), patterns di dua arm pertama tidak bakal ada yang cocok, jadi nilainya bakal cocok sama si garis bawah (underscore). Kita tidak memperkenalkan variabel x di dalam pattern buat arm garis bawah ini, jadi si x di ekspresi tersebut tetap merujuk pada x yang ada di luar yang belum tertimpa (unshadowed). Di kasus hipotetis (seandainya) ini, si match bakal mencetak Default case, x = None.

Begitu ekspresi match ini selesai, scope-nya berakhir, dan scope buat si y internal tadi juga berakhir. println! terakhir menghasilkan at the end: x = Some(5), y = 10.

Buat membikin ekspresi match yang bisa membandingkan nilai-nilai dari x dan y yang ada di luar, ketimbang memperkenalkan variabel baru yang malah menimpa variabel y yang sudah ada, kita wajib memakai kondisional match guard sebagai gantinya. Kita bakal ngebahas soal match guards nanti di “Kondisional Tambahan Memakai Match Guards”.

Multiple Patterns (Banyak Pola Sekaligus)

Di dalam ekspresi match, kita bisa mencocokkan banyak patterns sekaligus dengan memakai sintaks |, yang mana merupakan operator or (atau) buat pattern. Misalnya, di kode berikut ini kita mencocokkan nilai x terhadap match arms-nya, yang mana arm pertamanya punya sebuah opsi or, yang berarti kalau nilai dari x itu cocok sama salah satu dari nilai yang ada di arm itu, kode milik arm itu bakal dijalankan:

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("one or two"),
        3 => println!("three"),
        _ => println!("anything"),
    }
}

Kode ini mencetak one or two.

Mencocokkan Rentang (Ranges) Nilai Memakai ..=

Sintaks ..= memungkinkan kita buat mencocokkan dengan sebuah rentang nilai yang inklusif (inclusive range). Di kode berikut, saat sebuah pattern cocok sama nilai apa pun yang ada di dalam rentang yang ditentukan, arm tersebut bakal dieksekusi:

fn main() {
    let x = 5;

    match x {
        1..=5 => println!("one through five"),
        _ => println!("something else"),
    }
}

Kalau x itu adalah 1, 2, 3, 4, atau 5, arm pertama bakal cocok. Sintaks ini jauh lebih nyaman dipakai pas kita punya banyak nilai buat dicocokkan ketimbang memakai operator | buat mengekspresikan ide yang sama; kalau kita memakai |, kita harus menentukan 1 | 2 | 3 | 4 | 5. Menentukan sebuah rentang (range) itu jauh lebih singkat, apalagi kalau kita mau mencocokkan, katakanlah, angka apa aja antara 1 dan 1.000!

Compiler mengecek kalau rentang tersebut tidak kosong pas compile time, dan karena tipe-tipe yang mana Rust bisa tahu apakah sebuah rentang itu kosong atau tidak itu cuma char dan nilai-nilai numerik aja, maka dari itu rentang (ranges) cuma diizinkan buat nilai numerik atau char.

Berikut ini adalah contoh yang memakai rentang buat nilai char:

fn main() {
    let x = 'c';

    match x {
        'a'..='j' => println!("early ASCII letter"),
        'k'..='z' => println!("late ASCII letter"),
        _ => println!("something else"),
    }
}

Rust bisa tahu kalau 'c' berada di dalam rentang pattern pertama dan lalu mencetak early ASCII letter.

Destructuring (Membongkar) buat Memecah Nilai

Kita juga bisa memakai patterns buat men-destructure (membongkar/memecah belah) structs, enums, dan tuples supaya kita bisa memakai berbagai bagian-bagian yang berbeda dari nilai-nilai tersebut. Mari kita telusuri masing-masing dari mereka.

Destructuring Structs

Listing 19-12 menunjukkan sebuah struct Point yang punya dua fields (bidang), x dan y, yang bisa kita pecah memakai sebuah pattern dengan statement let.

Filename: src/main.rs
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}
Listing 19-12: Membongkar field-field sebuah struct ke dalam variabel-variabel yang terpisah

Kode ini membikin variabel a dan b yang masing-masing cocok dengan nilai dari field x dan y pada struct p. Contoh ini nunjukin kalau nama variabel-variabel di dalam pattern-nya tidak harus sama dengan nama-nama field di dalam struct-nya. Namun, sudah menjadi hal yang umum buat menyamakan nama variabelnya dengan nama field-nya supaya lebih gampang buat diingat variabel mana yang berasal dari field mana. Karena penggunaan umum ini, dan karena nulis let Point { x: x, y: y } = p; itu berisi sangat banyak duplikasi, Rust punya bentuk singkat (shorthand) buat patterns yang mencocokkan field struct: kita cuma perlu menyebutkan nama field struct-nya aja, dan variabel yang dibikin dari pattern tersebut bakal punya nama yang sama. Listing 19-13 berperilaku persis sama kayak kode di Listing 19-12, tapi variabel yang dibikin di pattern let-nya itu bernama x dan y bukannya a dan b.

Filename: src/main.rs
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}
Listing 19-13: Membongkar field struct memakai struct field shorthand (sintaks pendek field struct)

Kode ini membikin variabel x dan y yang cocok dengan field x dan y dari variabel p. Hasilnya adalah variabel x dan y itu sekarang berisi nilai-nilai yang ada dari struct p tersebut.

Kita juga bisa melakukan destructure memakai nilai literal (literal values) sebagai bagian dari pattern struct tersebut ketimbang membikin variabel buat semua field-nya. Melakukan hal ini memungkinkan kita buat ngetes (test) beberapa dari field tersebut terhadap suatu nilai tertentu sekaligus membikin variabel buat men-destructure field-field yang lain.

Di Listing 19-14, kita punya sebuah ekspresi match yang memisahkan nilai-nilai Point ke dalam tiga kasus: titik yang terletak persis di sumbu x (yang mana itu benar kalau y = 0), di sumbu y (x = 0), atau tidak di sumbu mana pun.

Filename: src/main.rs
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {x}"),
        Point { x: 0, y } => println!("On the y axis at {y}"),
        Point { x, y } => {
            println!("On neither axis: ({x}, {y})");
        }
    }
}
Listing 19-14: Membongkar dan mencocokkan nilai literal di dalam satu pattern

Arm pertama bakal cocok sama titik apa pun yang terletak di sumbu x dengan menentukan bahwa field y itu bakal dianggap cocok kalau nilainya cocok dengan literal 0. Pattern-nya tetap membikin variabel x yang mana bisa kita pakai di kode buat arm ini.

Serupa dengan itu, arm kedua mencocokkan titik apa pun yang ada di sumbu y dengan menentukan bahwa field x itu bakal dianggap cocok kalau nilainya 0 dan membikin sebuah variabel y buat nilai dari field y tersebut. Arm ketiga sama sekali tidak menentukan literal apa pun, jadi ia cocok buat Point yang mana aja dan membikin variabel buat kedua field x dan y.

Di contoh ini, nilai p itu cocok dengan arm kedua berkat fakta kalau x berisi nilai 0, jadi kode ini bakal mencetak On the y axis at 7.

Ingat bahwa sebuah ekspresi match bakal berhenti ngecek lengan-lengan (arms) lain begitu dia nemuin pattern pertama yang cocok, jadi biarpun Point { x: 0, y: 0} itu ada di sumbu x sekaligus ada di sumbu y, kode ini cuma bakal mencetak On the x axis at 0.

Destructuring Enums

Kita udah sering membongkar (destructured) enums di buku ini (misalnya, di Listing 6-5 di Bab 6), tapi kita belum pernah secara eksplisit ngebahas kalau pattern yang dipakai buat membongkar sebuah enum itu berhubungan erat dengan gimana cara data yang tersimpan di dalam enum tersebut didefinisikan. Sebagai contoh, di Listing 19-15 kita memakai enum Message dari Listing 6-2 lalu kita menulis sebuah match yang punya patterns buat membongkar setiap nilai internalnya.

Filename: src/main.rs
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.");
        }
        Message::Move { x, y } => {
            println!("Move in the x direction {x} and in the y direction {y}");
        }
        Message::Write(text) => {
            println!("Text message: {text}");
        }
        Message::ChangeColor(r, g, b) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
    }
}
Listing 19-15: Membongkar varian enum yang memegang berbagai macam nilai yang berbeda

Kode ini bakal mencetak Change color to red 0, green 160, and blue 255. Cobalah buat mengubah nilai dari msg buat melihat kode dari lengan-lengan yang lain itu jalan.

Buat varian enum yang tidak punya data apa pun, kayak Message::Quit, kita tidak bisa membongkar nilainya lebih jauh lagi. Kita cuma bisa mencocokkan terhadap literal nilai Message::Quit secara langsung, dan tidak ada variabel sama sekali di dalam pattern tersebut.

Buat varian enum yang bentuknya kayak struct (struct-like enum variants), seperti Message::Move, kita bisa memakai sebuah pattern yang mirip sama pattern yang kita tentukan buat mencocokkan structs. Setelah nama variannya, kita taruh kurung kurawal lalu kita sebutkan field-fieldnya beserta variabelnya supaya kita bisa memecah-mecah bagian-bagian itu buat dipakai di dalam kode untuk arm ini. Di sini kita memakai bentuk singkat (shorthand) sama seperti yang kita lakuin di Listing 19-13.

Buat varian enum yang bentuknya kayak tuple (tuple-like enum variants), kayak Message::Write yang menampung sebuah tuple dengan satu elemen dan Message::ChangeColor yang menampung sebuah tuple dengan tiga elemen, pattern-nya itu mirip sama pattern yang kita tentukan buat mencocokkan tuples. Jumlah variabel di dalam pattern-nya itu harus cocok dengan jumlah elemen di dalam varian yang lagi kita cocokkan tersebut.

Destructuring Structs dan Enums yang Bersarang (Nested)

Sejauh ini, contoh-contoh kita semuanya cuma mencocokkan structs atau enums dengan kedalaman satu level aja (one level deep), tapi pencocokan itu bisa juga lho dipakai buat item-item yang bersarang (nested items)! Misalnya, kita bisa merombak (refactor) kode yang ada di Listing 19-15 buat ngedukung warna RGB maupun HSV di dalam pesan ChangeColor, kayak yang ditunjukin di Listing 19-16.

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {
            println!("Change color to red {r}, green {g}, and blue {b}");
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) => {
            println!("Change color to hue {h}, saturation {s}, value {v}");
        }
        _ => (),
    }
}
Listing 19-16: Pencocokan pada enums yang bersarang (nested enums)

Pattern dari arm pertama di ekspresi match itu cocok dengan varian enum Message::ChangeColor yang mengandung varian Color::Rgb; dan kemudian pattern itu bakal mengikat dirinya (binds) ke ketiga nilai i32 internal tersebut. Pattern di arm kedua juga cocok dengan varian enum Message::ChangeColor, tapi enum internalnya itu cocok dengan Color::Hsv sebagai gantinya. Kita bisa menentukan kondisi-kondisi kompleks (complex conditions) ini semuanya di dalam satu ekspresi match tunggal, meskipun ada dua enum yang dilibatkan.

Destructuring Structs dan Tuples Bersamaan

Kita bisa nyampur (mix), mencocokkan (match), dan menyarangkan (nest) destructuring patterns dengan cara yang bahkan lebih kompleks lagi. Contoh berikut nunjukin proses destructure yang lumayan rumit di mana kita menyarangkan structs dan tuples di dalam sebuah tuple lalu kita mengekstrak (destructure) semua nilai primitifnya sampai keluar (out):

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }

    let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}

Kode ini membiarkan kita memecah tipe-tipe yang kompleks menjadi bagian-bagian komponen dasarnya (component parts) supaya kita bisa memakai nilai-nilai yang kita butuhkan secara terpisah.

Destructuring memakai patterns ini adalah cara yang nyaman sekali buat memakai potongan-potongan dari sebuah nilai, kayak misalnya nilai yang ada dari masing- masing field di dalam sebuah struct, secara terpisah dari satu sama lain.

Mengabaikan Nilai di dalam sebuah Pattern

kita udah melihat kalau kadang-kadang itu sangat berguna buat mengabaikan nilai- nilai di dalam sebuah pattern, kayak misalnya di arm terakhir dari sebuah match, buat ngedapetin catch-all yang tidak benar-benar ngelakuin apa-apa tapi tetep mempertimbangkan (account for) semua kemungkinan nilai yang tersisa. Ada beberapa cara buat mengabaikan seluruh nilai atau sebagian aja dari suatu nilai di dalam sebuah pattern: dengan memakai pattern _ (yang udah kita lihat), dengan memakai pattern _ di dalam pattern lainnya, dengan memakai nama yang diawali dengan garis bawah (underscore), atau dengan memakai .. buat mengabaikan semua sisa bagian dari sebuah nilai. Mari kita eksplorasi gimana dan kapan alasan yang tepat buat memakai masing-masing patterns ini.

Mengabaikan Seluruh Nilai Memakai _

Kita udah memakai garis bawah (underscore) sebagai wildcard pattern (pola yang bisa jadi apa saja) yang bakal cocok dengan nilai apa pun tapi tidak bakal mengikat dirinya (bind) ke nilai tersebut. Ini sangat berguna dipakai sebagai arm terakhir di dalam ekspresi match, tapi kita juga bisa memakainya di pattern mana aja lho, termasuk di parameter fungsi, kayak yang ditunjukin di Listing 19-17.

Filename: src/main.rs
fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {y}");
}

fn main() {
    foo(3, 4);
}
Listing 19-17: Memakai _ di dalam signature sebuah fungsi

Kode ini bakal bener-bener mengabaikan nilai 3 yang dioper sebagai argumen pertama, dan dia bakal mencetak This code only uses the y parameter: 4.

Di mayoritas kasus saat kita udah tidak butuh suatu parameter fungsi tertentu lagi, seharusnya kita sekalian aja ngubah signature-nya supaya dia tidak nyertain parameter yang tidak terpakai itu. Mengabaikan sebuah parameter fungsi bisa sangat sangat berguna di kasus-kasus di mana, misalnya, kita lagi mengimplementasikan sebuah trait saat kita diwajibkan buat punya type signature yang spesifik tapi isi fungsinya (function body) di implementasi kita sama sekali tidak butuh salah satu parameternya. Dengan melakukan itu kita bisa terhindar dari dapat peringatan (warning) compiler tentang parameter fungsi yang tidak terpakai (unused function parameters), yang mana peringatan itu bakal muncul kalau seandainya kita malah memakai sebuah nama.

Mengabaikan Bagian-bagian dari Sebuah Nilai Memakai _ yang Bersarang (Nested)

Kita juga bisa memakai _ di dalam pattern lain buat cuma ngabaiin sebagian dari sebuah nilai, misalnya, pas kita cuma pengen ngetes sebagian dari sebuah nilai tapi tidak punya alasan apa pun buat memakai bagian-bagian lainnya di dalam kode korespondensinya yang mau kita jalanin. Listing 19-18 nunjukin kode yang bertanggung jawab buat mengelola nilai dari suatu pengaturan (setting). Persyaratan bisnisnya adalah user tidak boleh ngubah (overwrite) penyesuaian (customization) yang udah ada pada suatu setting, tapi user boleh aja menghapus (unset) setting tersebut lalu ngasih dia nilai baru kalau setting tersebut saat itu lagi kosong (unset).

fn main() {
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);

    match (setting_value, new_setting_value) {
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    println!("setting is {setting_value:?}");
}
Listing 19-18: Memakai garis bawah di dalam patterns yang nyocokin varian Some pas kita tidak perlu buat memakai nilai di dalam Some tersebut

Kode ini bakal mencetak Can't overwrite an existing customized value dan terus setting is Some(5). Di match arm yang pertama, kita tidak perlu mencocokkan atau memakai nilai-nilai yang ada di dalam masing-masing varian Some, tapi kita benar-benar perlu buat ngetes kasus di mana setting_value dan new_setting_value dua-duanya adalah varian Some. Di kasus itu, kita mencetak alasan kenapa kita tidak mengubah setting_value, dan nilainya emang tidak jadi diubah.

Di semua kasus lainnya (kalau entah setting_value atau new_setting_value itu None) yang diekspresikan sama pattern _ di arm yang kedua, kita pengen ngebolehin new_setting_value buat ngegantiin (become) setting_value.

Kita juga bisa memakai garis bawah di banyak tempat di dalam satu pattern tunggal buat ngabaiin beberapa nilai tertentu. Listing 19-19 nunjukin sebuah contoh gimana caranya ngabaiin nilai kedua dan keempat di dalam sebuah tuple yang isinya ada lima item.

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {first}, {third}, {fifth}");
        }
    }
}
Listing 19-19: Mengabaikan beberapa bagian dari sebuah tuple

Kode ini bakal mencetak Some numbers: 2, 8, 32, dan nilai 4 dan 16 bakal diabaikan.

Mengabaikan Variabel yang Tidak Terpakai dengan Mengawali Namanya Memakai _

Kalau kita membikin sebuah variabel tapi tidak pernah memakannya di mana pun, Rust biasanya bakal mengeluarkan peringatan (warning) karena variabel yang tidak terpakai itu bisa aja merupakan sebuah bug. Namun, kadang-kadang itu kepake sekali buat bisa bikin sebuah variabel yang belum mau kita pakai sekarang, kayak pas kita lagi nge-prototipe (prototyping) atau pas lagi memulai sebuah project. Di situasi ini, kita bisa ngasih tahu Rust supaya tidak ngasih peringatan ke kita soal variabel yang tidak terpakai dengan ngawalin nama variabelnya pakai sebuah garis bawah (underscore). Di Listing 19-20, kita bikin dua variabel yang tidak dipakai, tapi pas kita mengompilasi kode ini, kita harusnya cuma bakal dapat satu peringatan aja tentang salah satunya.

Filename: src/main.rs
fn main() {
    let _x = 5;
    let y = 10;
}
Listing 19-20: Mengawali nama variabel dengan garis bawah buat ngehindarin peringatan variabel yang tidak terpakai

Di sini, kita dapat satu peringatan karena tidak memakai variabel y, tapi kita tidak dapat peringatan apa pun soal tidak memakai _x.

Perhatikan bahwa ada sedikit perbedaan yang halus (subtle difference) antara memakai _ sendirian dan memakai nama yang diawali dengan sebuah garis bawah. Sintaks _x itu tetap mengikat (binds) nilainya ke variabel tersebut, sementara _ tidak mengikat nilai itu sama sekali. Buat nunjukin di mana letak kasus yang mana perbedaan ini jadi penting, Listing 19-21 bakal ngasih kita sebuah error.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
        println!("found a string");
    }

    println!("{s:?}");
}
Listing 19-21: Sebuah variabel yang tidak terpakai dan diawali dengan sebuah garis bawah itu tetap mengikat nilainya, yang mana bisa aja ngambil kepemilikan (ownership) dari nilai tersebut.

Kita bakal nerima sebuah error karena nilai s tersebut bakal tetep aja di-move (dipindahkan) ke dalam _s, yang mana mencegah kita dari memakai s lagi setelah itu. Namun, memakai garis bawah itu sendiri aja tidak pernah mengikat (bind) ke nilainya. Listing 19-22 bakal berhasil di-compile tanpa error apa pun karena s tidak dipindahkan (moved) ke dalam _.

fn main() {
    let s = Some(String::from("Hello!"));

    if let Some(_) = s {
        println!("found a string");
    }

    println!("{s:?}");
}
Listing 19-22: Memakai sebuah garis bawah aja tidak bakal mengikat nilainya.

Kode ini bisa jalan dengan lancar karena kita bener-bener tidak pernah mengikat s ke mana pun; dia tidak dipindahkan.

Mengabaikan Sisa Bagian dari Sebuah Nilai Memakai ..

Buat nilai-nilai yang punya banyak bagian, kita bisa memakai sintaks .. buat cuma memakai bagian-bagian yang spesifik aja lalu ngabaiin sisanya, sehingga kita bisa menghindari nulisin garis bawah buat setiap nilai yang mau kita abaikan. Pattern .. mengabaikan bagian mana pun dari sebuah nilai yang belum kita cocokkan (matched) secara eksplisit di sisa pattern tersebut. Di Listing 19-23, kita punya struct Point yang memegang titik koordinat di ruang tiga dimensi (three-dimensional space). Di dalam ekspresi match, kita cuma mau beroperasi pada koordinat x doang dan ngabaiin nilai-nilai yang ada di field y dan z.

fn main() {
    struct Point {
        x: i32,
        y: i32,
        z: i32,
    }

    let origin = Point { x: 0, y: 0, z: 0 };

    match origin {
        Point { x, .. } => println!("x is {x}"),
    }
}
Listing 19-23: Mengabaikan semua field dari Point kecuali buat x dengan memakai ..

Kita nyebutin nilai x lalu setelah itu kita cuma masukin pattern .. aja. Ini jauh lebih cepet daripada kita harus nulisin y: _ dan z: _, terutama pas kita lagi ngerjain structs yang punya sangat banyak field di mana cuma ada satu atau dua field doang yang relevan buat kita.

Sintaks .. ini bakal melebar (expand) ke seberapa banyak pun nilai yang dibutuhin. Listing 19-24 nunjukin gimana cara memakai .. bareng sebuah tuple.

Filename: src/main.rs
fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}
Listing 19-24: Cuma mencocokkan nilai yang pertama dan yang terakhir dari sebuah tuple dan mengabaikan semua nilai-nilai lainnya

Di kode ini, nilai yang pertama dan yang terakhir bakal dicocokkan dengan first dan last. .. itu bakal nyocokin dan lalu ngabaiin semua hal yang ada di tengah-tengah mereka berdua.

Namun, pemakaian .. itu harus tidak ambigu (unambiguous). Kalau sampai tidak jelas nilai-nilai yang mana aja yang ditujukan buat dicocokin dan nilai yang mana yang seharusnya diabaikan, Rust bakal ngasih kita sebuah error. Listing 19-25 nunjukin sebuah contoh dari pemakaian .. secara ambigu, jadi dia tidak bakal bisa di-compile.

Filename: src/main.rs
fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {second}")
        },
    }
}
Listing 19-25: Sebuah percobaan buat memakai .. dengan cara yang ambigu

Pas kita men-compile contoh ini, kita dapet error ini:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:22
  |
5 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

error: could not compile `patterns` (bin "patterns") due to 1 previous error

Mustahil bagi Rust buat nentuin seberapa banyak nilai di dalam tuple tersebut yang harus diabaikan sebelum dia mencocokkan sebuah nilai dengan second lalu berapa banyak lagi nilai selanjutnya yang harus diabaikan setelahnya. Kode ini bisa aja berarti kalau kita mau ngabaiin 2, mengikat second ke 4, dan terus ngabaiin 8, 16, dan 32; atau bisa juga berarti kalau kita mau ngabaiin 2 dan 4, mengikat second ke 8, dan lalu ngabaiin 16 dan 32; dan seterusnya (and so forth). Nama variabel second itu tidak punya arti khusus apa-apa buat Rust, jadi kita dapet error compiler karena memakai .. di dua tempat kayak gini itu ambigu.

Kondisional Tambahan Memakai Match Guards

Sebuah match guard adalah tambahan kondisi if, yang ditentukan setelah pattern di dalam sebuah lengan (arm) match, yang wajib dicocokkan juga supaya arm tersebut bisa dipilih. Match guards ini berguna buat mengekspresikan ide-ide yang lebih kompleks daripada apa yang bisa diizinkan sama sebuah pattern sendirian. Namun, perhatikan bahwa mereka itu cuma tersedia di dalam ekspresi match, bukan di ekspresi if let atau while let.

Kondisi tersebut bisa memakai variabel yang dibikin di dalam pattern-nya. Listing 19-26 nunjukin sebuah match di mana arm pertamanya punya pattern Some(x) dan juga punya sebuah match guard if x % 2 == 0 (yang bakal jadi true kalau angkanya itu bilangan genap).

fn main() {
    let num = Some(4);

    match num {
        Some(x) if x % 2 == 0 => println!("The number {x} is even"),
        Some(x) => println!("The number {x} is odd"),
        None => (),
    }
}
Listing 19-26: Nambahin sebuah match guard ke dalam sebuah pattern

Contoh ini bakal mencetak The number 4 is even. Pas num dibandingkan dengan pattern di arm yang pertama, dia cocok karena Some(4) cocok dengan Some(x). Terus si match guard mengecek apakah sisa dari ngebagi x dengan 2 itu sama dengan 0, dan karena emang bener, arm yang pertama itu pun dipilih.

Kalau seandainya num tadi adalah Some(5), si match guard di arm pertama bakal jadi false karena sisa pembagian 5 dengan 2 itu adalah 1, yang mana tidak sama dengan 0. Rust terus bakal lanjut ke arm yang kedua, yang mana bakal cocok karena arm yang kedua itu tidak punya match guard dan makanya dia cocok buat varian Some yang mana aja.

Tidak ada cara buat mengekspresikan kondisi if x % 2 == 0 di dalam sebuah pattern secara langsung, jadi match guard ngasih kita kemampuan buat bisa mengekspresikan logika ini. Kekurangan dari penambahan fungsionalitas ekspresi (expressiveness) ekstra ini adalah kalau compiler tidak bakal mencoba buat mengecek kelengkapannya (exhaustiveness) saat ada ekspresi-ekspresi match guard yang dilibatkan.

Di Listing 19-11, kita sempat menyebutkan kalau kita bisa memakai match guards buat nyelesein masalah tertimpanya variabel (pattern-shadowing problem) yang kita alami. Ingat kembali kalau waktu itu kita membikin sebuah variabel baru di dalam pattern di ekspresi match-nya ketimbang memakai variabel yang udah ada di luar match. Variabel yang baru itu berarti kalau kita tidak bisa lagi melakukan pengetesan terhadap (test against) nilai dari variabel yang ada di luar itu. Listing 19-27 nunjukin gimana caranya kita bisa memakai sebuah match guard buat memperbaiki masalah ini.

Filename: src/main.rs
fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {x:?}"),
    }

    println!("at the end: x = {x:?}, y = {y}");
}
Listing 19-27: Memakai match guard buat ngetes kesamaan (equality) dengan sebuah variabel di luar

Kode ini sekarang bakal mencetak Default case, x = Some(5). Pattern di arm match yang kedua tidak memperkenalkan sebuah variabel baru y yang mana bakal menimpa y yang ada di luar, yang berarti kita bisa memakai y luar tersebut di dalam match guard-nya. Ketimbang menentukan pattern-nya sebagai Some(y), yang mana bakal menimpa y luar, kita menentukannya sebagai Some(n). Ini membikin sebuah variabel baru n yang tidak menimpa apa pun karena tidak ada variabel n di luar match.

Match guard if n == y itu bukanlah sebuah pattern dan makanya dia tidak memperkenalkan variabel baru apa pun. y ini adalah si y luar ketimbang sebuah y baru yang menimpanya, dan kita bisa nyari-nyari sebuah nilai yang punya nilai yang sama kayak si y luar tersebut dengan membandingkan n ke y.

Kita juga bisa memakai operator or | di dalam sebuah match guard buat menentukan lebih dari satu patterns; di mana kondisi dari match guard tersebut bakal berlaku buat kesemua patterns-nya. Listing 19-28 nunjukin hierarki (precedence) saat ngegabungin sebuah pattern yang memakai | dengan sebuah match guard. Bagian yang penting dari contoh ini adalah bahwa si match guard if y ini berlaku buat 4, 5, dan 6, biarpun dia mungkin kelihatannya kayak si if y ini cuma berlaku buat 6 doang.

fn main() {
    let x = 4;
    let y = false;

    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"),
    }
}
Listing 19-28: Menggabungkan banyak patterns sekaligus dengan sebuah match guard

Kondisi match itu menyatakan kalau arm tersebut cuma cocok kalau nilainya x itu sama dengan 4, 5, atau 6 dan kalau y itu true. Pas kode ini jalan, pattern dari arm pertama cocok karena x adalah 4, tapi si match guard if y itu false, jadi arm pertama tersebut tidak dipilih. Kodenya lanjut ke arm yang kedua, yang mana emang cocok, dan program ini mencetak no. Alasannya adalah karena kondisi if tersebut berlaku buat keseluruhan pattern 4 | 5 | 6, bukan cuma buat nilai terakhir 6 aja. Dengan kata lain, hierarki dari sebuah match guard sehubungan dengan (in relation to) sebuah pattern itu berperilaku kayak gini:

(4 | 5 | 6) if y => ...

bukannya kayak gini:

4 | 5 | (6 if y) => ...

Setelah menjalankan kodenya, perilaku hierarki ini terlihat dengan jelas: kalau match guard tersebut cuma diterapin ke nilai terakhir di daftar nilai yang ditentukan memakai operator |, arm tersebut pasti udah cocok dan programnya pasti bakal mencetak yes.

Binding Memakai @

Operator at @ membiarkan kita membikin sebuah variabel yang nampung sebuah nilai di waktu yang bersamaan dengan kita melakukan tes apakah nilai tersebut cocok sama suatu pattern (pattern match) atau tidak. Di Listing 19-29, kita mau ngetes apakah field id di sebuah Message::Hello itu ada di dalam rentang 3..=7. Kita juga mau mengikat (bind) nilai tersebut ke variabel id supaya kita bisa memakainya di dalam kode yang diasosiasikan (associated) sama arm tersebut.

fn main() {
    enum Message {
        Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello { id: id @ 3..=7 } => {
            println!("Found an id in range: {id}")
        }
        Message::Hello { id: 10..=12 } => {
            println!("Found an id in another range")
        }
        Message::Hello { id } => println!("Found some other id: {id}"),
    }
}
Listing 19-29: Memakai @ buat nge-bind ke suatu nilai di dalam sebuah pattern sembari juga ngetes (testing) nilai tersebut

Contoh ini bakal mencetak Found an id in range: 5. Dengan menspesifikasikan id @ sebelum rentang 3..=7, kita lagi menangkap nilai apa pun yang sekiranya cocok sama rentang itu lalu menaruhnya ke dalam sebuah variabel bernama id, dan sembari ngelakuin itu kita juga ngetes kalau nilai tersebut cocok sama pattern rentang (range pattern) tersebut.

Di arm yang kedua, di mana kita cuma menentukan sebuah rentang doang di dalam pattern-nya, kode yang terkait sama arm tersebut tidak punya variabel yang berisi nilai asli dari field id itu. Nilai dari field id itu bisa jadi adalah 10, 11, atau 12, tapi kode yang ada di arm pattern tersebut sama sekali tidak tahu nilainya yang mana. Kode di pattern tersebut tidak mampu buat memakai nilai dari field id, karena kita belum nyimpan nilai id tersebut di sebuah variabel.

Di arm terakhir, di mana kita udah menyebutkan sebuah variabel tanpa adanya rentang, kita bener-bener punya akses ke nilai yang bisa kita pakai di dalam kode untuk arm tersebut di sebuah variabel bernama id. Alasannya adalah karena kita udah memakai sintaks shorthand field struct. Tapi kita belum menerapkan tes (test) apa-apa ke dalam nilai di field id ini buat arm yang ini, seperti yang udah kita lakuin di dua arm pertama: nilai apa pun bakal cocok dengan pattern ini.

Memakai @ ngasih kita kemampuan buat ngetes sebuah nilai dan kemudian menyimpannya ke dalam sebuah variabel dalam satu pattern tunggal.

Ringkasan

Patterns di Rust itu sangat sangat berguna dalam membedakan (distinguishing) berbagai macam jenis data yang berbeda. Pas mereka dipakai di dalam ekspresi match, Rust memastikan kalau patterns kita udah mencakup semua kemungkinan nilai yang ada, karena kalau tidak program kita tidak bakal bisa di-compile. Patterns yang ada di dalam statement let dan parameter fungsi ngebikin konstruk-konstruk tersebut jadi jauh lebih berguna, memungkinkan proses memecah (destructuring) sebuah nilai jadi bagian-bagian yang lebih kecil dan meng-assign (ngasih nilai ke) bagian-bagian itu ke dalam berbagai variabel. Kita bisa bikin patterns yang simpel maupun yang kompleks buat menyesuaikan dengan apa yang kita butuhkan.

Berikutnya, untuk bab kedua dari akhir buku ini, kita bakal melihat beberapa aspek tingkat mahir (advanced aspects) dari berbagai macam fitur yang ada di Rust.