Jika Anda telah mengikuti saya selama beberapa waktu, Anda pasti tahu bahwa saya benar-benar mulai menikmati Rust pada tahun lalu. Karat memiliki banyak fitur hebat, dan pencocokan pola adalah salah satunya. Jika Anda pernah menggunakan bahasa lain seperti Haskell atau Standard ML, Anda akan melihat beberapa kesamaan. Sama dengan pencocokan pola dasar lengkap saat di Kotlin (dengan beberapa pekerjaan kecil). Pencocokan pola di Rust menghasilkan kode yang ekspresif, mudah dibaca, dan jelas. Saya akui bahwa cara Rust melakukannya adalah favorit pribadi saya. Pada artikel ini kita akan melihat topik ini, dan mungkin Anda akan mengerti mengapa menurut saya ini sangat bagus!

Dasar-dasar pola

Dapat disangkal vs tidak dapat disangkal

Pola di Rust hadir dalam dua jenis; terbantahkan dan tidak terbantahkan. Pola yang cocok secara kondisional disebut dapat disangkal, sedangkan pola yang cocok dengan nilai apa pun yang mungkin disebut tidak dapat disangkal. Yang mana yang dapat Anda gunakan bergantung pada konteksnya. Misalnya, pernyataan let memerlukan pola yang tidak dapat disangkal, karena apa yang akan terjadi jika variabel dalam pernyataan let tidak mendapatkan nilai?

// Irrefutable patterns
let x = 2;
let (x, y) = (2, 3);

// WILL NOT COMPILE
// let does not allow refutable patterns
let Ok(x) = someString.parse::<i32>()
// trying to parse a string will return a Result, and can not guarantee that a result and not an Err is returned

Sebaliknya, pernyataan “jika dibiarkan” dapat memiliki pola yang dapat disangkal, karena isi dievaluasi secara kondisional:

if let Ok(x) = someString.parse::<i32>() {
    // … do something if someString can be parsed as a 32 bit integer …
}

// if let can have a refutable pattern, so we can also use a value for x:
if let Ok(64) = someString.parse::<i32>() {
    // … do something if someString can be parsed as a 32 bit integer …
}

Kita akan melihat lebih banyak contoh pernyataan dan pola semacam ini di bawah. Jika Anda masih menganggap topik “terbantahkan vs tak terbantahkan” itu sulit, baca kembali bagian ini setelah membaca sisa artikel. Dokumentasi Rust juga memiliki beberapa contoh dan penjelasan yang layak untuk dibaca.

Destrukturisasi

Banyak pola yang merupakan pola yang merusak berbagai jenis, dan juga dapat dipadupadankan. Mari kita lihat beberapa di antaranya.

Tuple
Kita telah melihat contoh destrukturisasi tuple di bagian terakhir, namun mari kita lihat lagi:

// myTuple is of type (i32, i32, &str)
let my_tuple = (1, 2, "hellothere");
let (x, y, my_str) = my_tuple;

Di sini kita melihat di baris terakhir bahwa my_tuple dipecah menjadi 3 variabel baru: x, y, dan my_str. Hal ini dapat dilakukan untuk semua jenis tupel, selama tipe yang dirusak cocok.

Anda juga dapat mencocokkan elemen dengan “..” (mungkin beberapa) atau “_” (tunggal), yang sering digunakan untuk melewati elemen:

// ignore my_str
let (x, y, _) = my_tuple;

// ignore everything after x
let (x, ..) = my_tuple;

// bigger tuple
let bigger_tuple = (1, 2, 3, 4, 5);

// get first and last
let (first, .., last) = bigger_tuple;

// ambiguous! NOT ALLOWED
// How would the compiler even know which element you wanted
let (.., middle, ..) = bigger_tuple;

(perhatikan bahwa polanya harus jelas)

Anda mungkin mencoba contoh Tuple pertama di atas dengan struct Tuple dan mendapatkan pesan kesalahan. Hal ini karena struct tuple memiliki lebih banyak kesamaan dengan destrukturisasi struct, sehingga memerlukan sintaksis khusus:

// Defining a tuple struct that looks like the tuple in the previous example:
struct MyTuple(i32, i32, String);

// Destructure it
let my_tuple = TupleStr(1, 2, "hellothere".to_string());
let TupleStr(x, y, my_str) = my_tuple;

(perhatikan bahwa kita harus mengubah tipe &str menjadi String, karena kompiler tidak dapat menyimpulkan ukuran &str mana pun yang mungkin kita inginkan gunakan di struct Tuple kami. String disimpan di heap, sehingga memecahkan masalah itu).

Tuple struct secara teknis adalah struct, yang membawa kita ke jenis penghancuran berikutnya…

Struktur
Struktur tidak jauh berbeda, dan sebuah contoh mungkin menunjukkannya dengan jelas:
#+BEGIN_SRC berkarat

#+END_SRC

// define a simple struct
struct Point {
    x: f32,
    y: f32,
    z: f32
}

// create a variable to use
let myPoint = Point {
    x: 1.0,
    y: 0.5,
    z: -1.0
};

// destructure it!
let Point { x, y, z} = my_point;

// Maybe we just want x and y?
let Point { x, y, .. } = my_point;

// or maybe just z
let Point { z, .. } = my_point;

Satu hal yang harus Anda perhatikan ketika mendestrukturisasi struct adalah bahwa namanya harus cocok dengan yang ditemukan di struct, dan “..” harus berada di urutan terakhir. “..” dalam hal ini berarti mencocokkan sisanya dan mengabaikan hasilnya.

Enum
Kasus paling sederhana untuk enum adalah mencocokkan enum tanpa data:

// define a simple enum
enum Color {
    Red,
    Blue,
    Green
}

// match if our color is green
if let Color::Green = my_color {
    // .. do something is color is green ..
}

Ini tidak terlalu menarik, karena Rust enum jauh lebih kuat. Jika Anda belum familiar dengannya, mereka mungkin berisi data! Mari kita lihat contohnya:

// More advanced enum
enum HttpRequest {
    Get,
    Post(String)
}

// match the post request
if let HttpRequest::Post(data) = my_request {
    // .. do something with the post request data …
}

// can also ignore data
if let HttpRequest::Post(_) = my_request {
    // .. do something when post request …
}

Jika enum memiliki beberapa argumen, Anda dapat melakukan sebagian besar dari apa yang biasa Anda lakukan dari tupel di atas. Anda dapat menggunakan rentang (misalnya, “1..=2”), ​​atau melewatkan beberapa elemen dengan “..” saja (misalnya, “MyEnum( firstElem, .., lastElem)”).

Gabungan
Anda juga dapat menggabungkan semua hal di atas ke dalam pola Anda sendiri! Struktur di dalam enum, enum di dalam tupel, dll. Daftarnya terus bertambah!

// Define some nested structure
enum Color {
    Red,
    Blue,
    Green
}

// imagine old OpenGL from the early 2000s where colors of points were interpolated across the shape
struct Point {
    x: f32,
    y: f32,
    z: f32,
    color: Color
}

struct Triangle(Point, Point, Point);

// A destructuring based upon the data we want
// get only x for the first point when the first points color is blue
if let Triangle(
    Point {
        x,
        color: Color::Blue, ..
    },
    ..,
 ) = my_triangle {
     // .. do something with the data we wanted for some reason ..
 }

Pola lainnya

Ada jenis pola lain juga, dan ini sebagian besar digunakan dengan kecocokan yang akan Anda lihat lebih lanjut di bawah. Yang pertama adalah or-matcher:

// matches 1, 2 or 3
1 | 2 | 3

// Matches one of the strings
"first" | "second"

Kami melihat rentang secara singkat di atas:

// matches 1 to 10 (inclusive)
1..=10

// matches 1 to 10 (non-inclusive)
1..10

(rentang juga dapat digunakan sebagai indeks array untuk mengambil banyak elemen)

Pola terakhir yang ingin saya tunjukkan adalah pola yang digunakan untuk menguji dan menangkap nilai. Ya, Anda dapat memiliki keduanya! Fungsionalitas ini sering digunakan untuk menangkap nilai di suatu tempat di dalam pola yang memenuhi kondisi tertentu, dan dapat digunakan jauh di dalam pola. Mari kita lihat contoh dengan rentang:

// Integer version of point
struct MyData {
    x: i32,
    y: i32,
    z: i32,
}

// Match data x value when it's between 1 and 20 (inclusive)
if let MyData {
    x: my_x @ 1..=20, ..
} = my_data
{
    // .. do something with the my_x that is 1<=20 ..
}

(perhatikan bahwa variabel yang kita gunakan sekarang disebut my_x)

Ini dapat digunakan dengan if-let dan tempat lain yang memungkinkan pola yang dapat disangkal, namun sebagian besar digunakan bersama dengan pola lain yang cocok. Saya jarang, jika pernah menggunakan salah satu dari ini dengan if-let (mungkin dengan pengecualian rentang).

Itu seharusnya menjadi topik yang paling penting, tapi mungkin ada beberapa hal yang saya lewatkan. Bagaimanapun, Rust adalah bahasa yang terus berkembang, dan fitur-fitur baru mungkin telah ditambahkan. Mungkin juga ada fitur yang jarang saya gunakan, sehingga saya lupakan. Anda dapat melihat dokumentasi Rust tentang topik ini untuk mengetahui lebih lanjut.

Peringatan

Satu peringatan kecil yang saya temukan adalah bahwa Rust rewel tentang floating point dalam pola. Bukan tentang bagian penangkapannya, tetapi jika Anda mencoba menggunakannya dalam jangkauan dan sebagainya. Ini memperingatkan Anda bahwa itu tidak didukung. Kita semua tahu bagaimana perilaku floating point, dan bahwa floating point sangat sensitif terhadap skala yang berbeda dan jarang (jika pernah) tepat. Itu mungkin menjelaskan mengapa mereka tidak disarankan untuk digunakan dengan pola Rust yang lebih canggih.

Di mana menggunakan pencocokan pola? (contoh waktu!)

Kita telah melihat beberapa contoh dasar di atas, tapi mari selami lebih dalam dan lihat bagaimana dan di mana menggunakan pencocokan pola. Kita telah melihat banyak pernyataan let dan if-let, jadi mari kita lihat kecocokan dan pola pada tanda fungsi!

cocok

Menurut pandangan saya, ini adalah pernyataan saklar/kasus yang biasa Anda gunakan dalam bahasa lain, tetapi pada steroid! kecocokan memungkinkan Anda menulis pencocokan yang sangat kuat. Kita telah melihat semua pola berbeda di atas, jadi mari kita lihat beberapa contoh singkat:

// A more extensive version of httprequest
enum HttpRequest {
    Get,
    Post(String),
    Put(String),
    Custom,
    Unknown
}

// a match expression
match my_request {
    Get => {
        // .. do something with get ..
    }
    Post(data) | Put(data) => {
        // .. do something with the data ..
    }
    _ => {}
}

(kecocokan harus menyeluruh, dan mencakup semua kasus. Anda lihat kami menangani ini dengan wildcard “_” di atas. Ini juga merupakan keuntungan, karena kompiler membantu Anda mengingat semua kasus!)

korek api juga punya benda pintar lainnya yang disebut penjaga korek api! Ini pada dasarnya adalah kalimat if tambahan di dalam case. Mari tambahkan ke kasus kedua di atas:

match my_request {
    HttpRequest::Get => {
        // .. do something with get ..
    }
    HttpRequest::Post(data) | HttpRequest::Put(data) if !data.is_empty() => {
        // .. do something with the data ..
    }
    _ => {}
};

Di sini kami telah menambahkan tanda centang untuk melihat apakah data string yang dikirim tidak kosong! Cukup bagus!

fungsi

Anda dapat menggunakan pola dalam definisi fungsi! Itu sangat keren, bukan? Anda harus ingat bahwa polanya harus tidak dapat disangkal. Jangan sedih dulu! Ini tetap berarti Anda dapat menggunakan banyak operasi penghancuran yang telah kita lihat di atas! Mari kita lihat beberapa contoh:

// test data
struct Vector2d {
    x: f32,
    y: f32
}

// destructured Vector2d
fn length(Vector2d { x, y }: Vector2d) -> f32 {
    (x.powi(2) + y.powi(2)).sqrt()
}

fn x_val(Vector2d { x, .. }: Vector2d) -> f32 {
    x
}

Mereka juga bisa mendalam jika Anda mau, tapi ingatlah untuk tidak mengacaukan keterbacaan kode Anda.

Jangan ragu untuk membagikan pencocokan pola cerdas apa pun yang telah Anda lakukan pada tanda tangan fungsi Anda di komentar! Dan ya, saya tahu Anda juga memilikinya dalam JavaScript.

Sekarang semoga Anda telah melihat sedikit tentang apa yang bisa dilakukan pola di Rust. Jika Anda baru mengenal Rust, mungkin itu menginspirasi Anda untuk mempelajari bahasanya? :)

Awalnya diterbitkan di https://themkat.net pada 6 Oktober 2022.