body{font-family:Arial,sans-serif;line-height:1.6;margin:0;padding:20px;background:#f9f9f9;color:#333}
h1{color:#2c3e50;font-size:2.2em;margin-bottom:0.5em}
h2{color:#34495e;font-size:1.8em;margin-top:1.5em}
h3{color:#4a6d8c;font-size:1.5em;margin-top:1.2em}
p{margin:0.8em 0}
ul{margin:0.8em 0 0.8em 1.5em}
code{background:#eef; padding:2px 4px;border-radius:3px}
pre{background:#eef;padding:10px;border-radius:4px;overflow:auto}
a{color:#2980b9;text-decoration:none}
a:hover{text-decoration:underline}
.container{max-width:800px;margin:auto;background:#fff;padding:30px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.1)}
Trong hành trình Lập trình Rust Cơ Bản, generics là một trong những công cụ mạnh mẽ nhất giúp bạn viết code linh hoạt và tái sử dụng một cách tối ưu. Bài viết này sẽ giải thích khái niệm, cách sử dụng và cung cấp các ví dụ thực tế để bạn nhanh chóng áp dụng vào dự án của mình.
1. Generics là gì?
Generics (kiểu tổng quát) trong Rust cho phép bạn định nghĩa các hàm, struct, enum mà không cần chỉ định kiểu dữ liệu cụ thể ngay từ đầu. Thay vào đó, kiểu sẽ được “đổ” (instantiate) khi bạn gọi chúng, giúp giảm thiểu việc lặp lại mã và tăng tính tái sử dụng.
2. Tại sao cần Generics trong Rust?
- Code linh hoạt: Một hàm hoặc struct có thể làm việc với nhiều kiểu dữ liệu khác nhau.
- An toàn về kiểu: Rust vẫn giữ được kiểm tra kiểu tại thời điểm biên dịch, tránh lỗi runtime.
- Tối ưu hiệu năng: Không có overhead của polymorphism động; mọi thứ được tối ưu ở compile-time.
- Dễ bảo trì: Giảm số lượng hàm lặp lại, làm cho dự án sạch sẽ hơn.
3. Cú pháp cơ bản của Generics
3.1. Hàm generic
fn print_value(value: T) {
println!("Giá trị: {}", value);
}
Ở đây T là một placeholder cho bất kỳ kiểu nào thực hiện trait Display. Hàm print_value có thể nhận i32, f64, String, …
3.2. Struct generic
struct Pair<T, U> {
first: T,
second: U,
}
impl<T, U> Pair<T, U> {
fn new(first: T, second: U) -> Self {
Self { first, second }
}
}
4. Các trait bound phổ biến
Để giới hạn các kiểu có thể được sử dụng, bạn dùng trait bounds. Dưới đây là một số ví dụ thường gặp:
T: Clone– Kiểu phải hỗ trợ phương thứcclone().T: PartialOrd– Kiểu có thể so sánh thứ tự.T: std::ops::Add<Output = T>– Kiểu hỗ trợ phép cộng.
5. Ví dụ thực tế: Stack generic
Hãy xây dựng một Stack có thể chứa bất kỳ kiểu dữ liệu nào.
#[derive(Debug)]
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Self { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn peek(&self) -> Option<&T> {
self.items.last()
}
}
fn main() {
let mut int_stack = Stack::new();
int_stack.push(10);
int_stack.push(20);
println!("Stack int: {:?}", int_stack);
let mut str_stack = Stack::new();
str_stack.push(String::from("rust"));
str_stack.push(String::from("generics"));
println!("Stack string: {:?}", str_stack);
}
Đoạn code trên minh hoạ cách viết code linh hoạt và tái sử dụng một cấu trúc dữ liệu mà không cần viết lại cho mỗi kiểu dữ liệu.
6. Khi nào nên tránh Generics?
Mặc dù generics mạnh mẽ, nhưng không phải lúc nào cũng là lựa chọn tốt nhất. Hãy cân nhắc:
- Quá nhiều bound phức tạp có thể làm mã khó đọc.
- Nếu chỉ cần một kiểu duy nhất, việc dùng generic có thể gây dư thừa.
- Trong các trường hợp cần dynamic dispatch, trait objects (
Box<dyn Trait>) có thể phù hợp hơn.
7. Tài nguyên học thêm
Kết luận
Generics là một phần cốt lõi của Lập trình Rust Cơ Bản giúp bạn viết code linh hoạt, tái sử dụng và vẫn giữ được hiệu năng tuyệt vời của Rust. Bằng cách nắm vững các khái niệm như trait bound, impl generic và các ví dụ thực tế, bạn sẽ tự tin áp dụng generics vào mọi dự án, từ những chương trình nhỏ đến các hệ thống phức tạp.
Hãy thử tạo một vài struct hoặc hàm generic ngay hôm nay và cảm nhận sự khác biệt! Nếu bạn có câu hỏi hoặc muốn chia sẻ kinh nghiệm, đừng ngần ngại để lại bình luận bên dưới.