1 tháng học Rust - Enums và Pattern Matching

1 tháng học Rust - Enums và Pattern Matching

Vấn đề

Về enums - một cấu trúc dữ liệu liệt kê mà có thể ai cũng biết. Enums cho phép xác định một loại bằng cách liệt kê các biến thể có thể có của nó. Trong mỗi ngôn ngữ lập trình, enums có cách khai báo và sử dụng khác nhau. Đối với Rust, enums được sử dụng khá thường xuyên vì nhiều lợi ích mà nó mang lại.

Bài viết ngày hôm nay chúng ta sẽ cùng nhau tìm hiểu về enums, xem nó có gì hay ho hơn so với các ngôn ngữ khác không nhé!

Enums

Một enums được khai báo như sau:

enum IpAddrKind {
    V4,  
    V6,  
}

Sau đó có thể sử dụng:

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

Enums trong Rust cũng lưu trữ được dữ liệu.

enum IpAddr {
    V4(String),  
    V6(String),  
}

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));

Thậm chí, chúng ta còn có thể khai báo thêm phương thức cho nó.

enum IpAddr {
    V4(String),  
    V6(String),  
}

impl IpAddr {
    fn call(&self) {
        println!("Hello from IpAddr!");
    }
}

let home = IpAddr::V4(String::from("127.0.0.1"));
home.call();

Option là một enums được định nghĩa sẵn ở trong Rust, nó có một sứ mệnh quan trọng trong việc xử lý dữ liệu null.

Một Option có dạng Generic như sau.

enum Option<T> {
    None,  
    Some(T),  
}

Nhiều ngôn ngữ lập trình khác, chúng ta quá quen thuộc với kiểu dữ liệu null đại diện cho việc không có giá trị nào. Rust không có kiểu dữ liệu null, hầu hết trường hợp cần sử dụng null thì sẽ chuyển qua sử dụng enums Option.

None đại diện cho “không có gì” còn Some đại diện cho sự hiện hữu của một dạng dữ liệu nào đó. Lấy ví dụ.

let none: Option<i8> = None;
let five: Option<i8> = Some(5);
let six: Option<i8> = Sone(6);

Và dĩ nhiên chúng ta không thể thực hiện các phép tính trực tiếp trên kiểu dữ liệu Option này một cách bình thường.

// error
let sum = none + five;

// error
let sum = five + six;

Vậy thì làm cách nào để sử dụng được enums nói chung cũng như Option nói riêng?

Matching

Hầu hết việc xác định enums nhằm mục đích phân loại và xử lý dữ liệu theo các dạng được định nghĩa từ trước, chính vì thế Rust có cấu trúc xử lý loại rất mạnh mẽ là match hay còn gọi là “khớp mẫu”.

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,  
    }
}

Hàm value_in_cents nhận vào một enums có kiểu Coin từ đó trả về giá trị tương ứng với tên của các đồng xu bằng cách sử dụng cú pháp match.

Đối với Option, chúng ta cũng sử dụng match để xử lý các trường hợp NoneSome, cũng như thực hiện các phép tính cơ bản mà Some nắm giữ như ví dụ ở đầu bài viết.

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);

Hàm plus_one nhận vào một Option<i32> và cộng thêm 1 vào giá trị của nó. Bằng cách khớp mẫu, hàm trả về None nếu như x là None, ngược lại nếu x là loại Some, tức là nó có giá trị thì sẽ trả về giá trị Some mới bằng cách cộng thêm 1.

Cho dễ hình dung, match gần giống với lệnh swich…case trong JavaScript. Trong khi switch đưa ra một giá trị khi nào khớp với giá trị trong case thì hàm xử lý đó ngay lập tức được thực thi. Nếu như switch…case có trường hợp default, nghĩa là khi các giá trị trong tất cả case không khớp thì cuối cùng hàm xử lý default được thực hiện. Trong Rust cũng có, người ta gọi là “Placeholder” hay “trình giữ chỗ”.

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) {}

Quay trở lại với trường hợp sử dụng Option, đôi khi chúng ta không quan tâm đến None lắm mà chỉ đơn giản là muốn thao tác với dữ liệu nếu có của Some. Ví dụ dưới đây luôn phải khai báo thêm một trình giữ chỗ để “không phải làm gì”.

let config_max = Some(3u8);
match config_max {
    Some(max) => println!("The maximum is configured to be {}", max),  
    _ => (),  
}

Điều này lặp lại một cách nhàm chán và thừa thãi, thay vào đó, Rust gợi ý chúng ta sử dụng kết hợp iflet để chỉ xử lý riêng cho một trường hợp cụ thể của một enums.

let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is configured to be {}", max);
}

Cuối cùng, vẫn có một lưu ý quan trọng về quyền sở hữu khi khớp mẫu khi Some giữ các giá trị tham chiếu. Bạn đọc có thể tham khảo thêm tại The Rust Programming Language - How Matches Interact with Ownership.

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.
Author

Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.

Bạn thấy bài viết này có ích?
Không

Bình luận (1)

Avatar
Ẩn danh11 tháng trước
Mình cũng bắt đầu tìm hiểu về rust, bạn có muốn lập nhóm ngồi cf trao đổi để học không?
Trả lời
Avatar
Xuân Hoài Tống11 tháng trước
Chào bạn, rất vui khi nhận được lời đề nghị của bạn. Mình viết ra series tự học Rust để tạo cam kết cũng như ghi chép lại những gì học được, qua đó tóm tắt lại một cách cô đọng cho người đọc tham khảo. Tuy nhiên thì mình có nhiều lý do để rất khó lập được nhóm và cùng nhau trao đổi, như công việc và gia đình. Nhưng không sao, mình vẫn có thời gian cuối ngày để viết blog cũng như rất sẵn sàng trao đổi với bạn (dù không được realtime cho lắm). Hoặc nếu bạn cũng viết blog thì có thể để lại địa chỉ để cùng nhau học hỏi thêm :D