Tổ Chức Dữ Liệu Nâng Cao Trong Rust: Struct, Enum Và Sức Mạnh Của Pattern Matching

Sau khi đã làm chủ cách quản lý bộ nhớ qua Ownership và chia sẻ dữ liệu an toàn bằng Borrowing, chúng ta sẽ chuyển sang một bước tiến quan trọng trong tư duy thiết kế phần mềm: Cách tự định nghĩa các kiểu dữ liệu phức hợp để mô hình hóa thế giới thực trong Rust.
Nếu như các kiểu dữ liệu như Tuple hay Array chỉ phù hợp với các tập hợp dữ liệu đơn giản, thì Struct và Enum lại chính là "xương sống" để xây dựng nên những kiến trúc hệ thống lớn, mạch lạc và dễ bảo trì. Hãy cùng mổ xẻ chi tiết ngay dưới đây.
🏛️ 1. Struct (Cấu trúc dữ liệu tùy chỉnh)
Struct cho phép bạn gom nhóm nhiều giá trị có kiểu dữ liệu khác nhau lại với nhau thành một thực thể có ý nghĩa rõ ràng (giống như các đối tượng hoặc interface trong các ngôn ngữ lập trình khác). Mỗi một phần tử bên trong struct được gọi là một field (trường dữ liệu).
A. Khai báo và khởi tạo Struct
Để định nghĩa một struct, bạn dùng từ khóa struct, đặt tên theo chuẩn PascalCase (viết hoa chữ cái đầu mỗi từ), sau đó liệt kê các field kèm theo kiểu dữ liệu tương ứng.
// Định nghĩa một Struct đại diện cho thông tin người dùng
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
// Khởi tạo một thể hiện (instance) của User
let user1 = User {
email: String::from("mrseadev@example.com"),
username: String::from("MrSea Dev"),
active: true,
sign_in_count: 1,
};
// Truy xuất dữ liệu bên trong struct bằng cú pháp dấu chấm (.)
println!("User email: {}", user1.email);
}
Lưu ý: Giống như biến thông thường, khi bạn muốn thay đổi giá trị của một field bên trong struct, toàn bộ thể hiện của struct đó khi khởi tạo bắt buộc phải được đánh dấu là thay đổi được bằng từ khóa mut (let mut user1 = User { ... };).
B. Thêm phương thức cho Struct với khối impl
Rust không sử dụng lập trình hướng đối tượng cổ điển với lớp (class) và sự kế thừa. Thay vào đó, bạn gắn kết các hàm xử lý trực tiếp vào struct thông qua khối Implementation (impl). Hàm đầu tiên nhận vào tham số &self chính là các phương thức của đối tượng.
struct Rectangle {
width: u32,
height: u32,
}
// Khối impl chứa các phương thức gắn liền với Rectangle
impl Rectangle {
// Phương thức tính diện tích (nhận tham chiếu bất biến &self)
fn area(&self) -> u32 {
self.width * self.height
}
// Associated Function (Hàm liên kết, tương tự phương thức static) dùng để khởi tạo nhanh
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50 };
println!("Diện tích hình chữ nhật là: {}", rect.area());
let sq = Rectangle::square(20);
println!("Diện tích hình vuông là: {}", sq.area());
}
🔀 2. Enum (Kiểu liệt kê) và Khả năng mô hình hóa mạnh mẽ
Enum trong Rust không đơn thuần chỉ là danh sách các con số nguyên đại diện cho các trạng thái như ở C hay Java. Enum của Rust mạnh mẽ hơn rất nhiều nhờ khả năng gắn kèm dữ liệu trực tiếp vào bên trong từng biến thể (variant).
Hãy xét một ví dụ thực tế về việc mô hình hóa các trạng thái kết nối mạng hoặc giao diện UI:
// Định nghĩa một Enum có chứa các cấu trúc dữ liệu khác nhau bên trong
#[derive(Debug)]
enum Message {
Quit, // Không chứa dữ liệu
Move { x: i32, y: i32 }, // Chứa các field dạng named struct
Write(String), // Chứa một chuỗi String đơn lẻ
ChangeColor(i32, i32, i32), // Chứa một tuple gồm 3 số nguyên
}
fn main() {
let msg = Message::Write(String::from("Hello Rust!"));
// In ra trạng thái sử dụng trait Debug
println!("{:#?}", msg);
}
🎯 3. Pattern Matching với từ khóa match
Để xử lý và bóc tách dữ liệu từ các biến thể phức tạp của Enum, Rust cung cấp một công cụ kiểm khớp mẫu cực kỳ mạnh mẽ mang tên match.
Điểm ăn tiền của match là tính bao phủ hoàn toàn (exhaustiveness): Trình biên dịch Rust bắt buộc bạn phải xử lý tất cả mọi trường hợp có thể xảy ra của Enum. Nếu bạn bỏ sót dù chỉ một trường hợp, mã nguồn sẽ không thể biên dịch được, giúp bạn không bao giờ phải đối mặt với lỗi thiếu logic ngầm.
enum Coin {
Penny,
Nickel,
Dime,
Quarter(String), // Đồng xu đặc biệt mang theo tên tiểu bang phát hành
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Đồng xu may mắn!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("Đồng xu quý tộc từ bang: {state}!");
25
}
}
}
fn main() {
let my_coin = Coin::Quarter(String::from("California"));
let value = value_in_cents(my_coin);
println!("Giá trị quy đổi: {value} xu.");
}
Sử dụng cú pháp rút gọn if let
Đôi khi việc dùng match tỏ ra quá dài dòng nếu bạn chỉ muốn bắt một trường hợp duy nhất và lờ đi tất cả các trường hợp còn lại. Lúc này, if let là sự thay thế cực kỳ thanh lịch:
fn main() {
let config_max = Some(3u8);
// Thay vì viết một khối match dài dòng, ta dùng if let
if let Some(max) = config_max {
println!("Giá trị cấu hình tối đa là: {max}");
}
}
Thông qua Struct, Enum và Pattern Matching, bạn đã có trong tay những công cụ thiết kế dữ liệu chuẩn mực để xây dựng các logic nghiệp vụ phức tạp.
Tuy nhiên, trong quá trình lập trình thực tế, việc các hàm phát sinh lỗi là điều không thể tránh khỏi. Các ngôn ngữ khác thường dùng ngoại lệ (try/catch) hoặc giá trị null đầy rủi ro. Vậy Rust giải quyết bài toán này như thế nào? Hãy cùng khám phá ở Bài 7: Nói không với Null — Xử lý lỗi an toàn thông qua Option<T> và Result<T, E>!
Bình luận