Trait và Generics Trong Rust (Lập Trình Tổng Quát Và Định Nghĩa Hành Vi Chung)

lúc 18:21 30 tháng 8, 2026
19 views
Trait và Generics Trong Rust (Lập Trình Tổng Quát Và Định Nghĩa Hành Vi Chung)

Sau khi đã nắm vững các kiểu dữ liệu cơ bản, cấu trúc và cách xử lý lỗi, chúng ta bước vào một trong những chương quan trọng nhất để viết mã nguồn mở rộng (scalable) và tái sử dụng cao trong Rust: Generics (Lập trình tổng quát)Traits (Giao diện hành vi).

Nếu bạn từng quen thuộc với Interfaces trong Java/Go hay Classes trừu tượng trong C++, tư duy về Trait của Rust sẽ mang lại một góc nhìn hoàn toàn mới: mạnh mẽ hơn, mạch lạc hơn và không có chi phí hiệu năng chạy ngầm (Zero-cost abstractions). Cùng mổ xẻ chi tiết ngay dưới đây.

🧬 1. Generics (Lập trình tổng quát)

Trong quá trình lập trình, bạn thường gặp trường hợp phải viết hai hàm có logic xử lý hệt nhau, chỉ khác mỗi kiểu dữ liệu đầu vào (ví dụ một hàm tìm số lớn nhất kiểu i32, một hàm tìm số lớn nhất kiểu f64). Generics sinh ra để giải quyết triệt để sự lặp lại này bằng cách cho phép bạn viết mã với các kiểu dữ liệu tượng trưng (thường đặt là chữ cái T).

A. Hàm tổng quát với Generics

Hãy xem cách chúng ta viết một hàm tìm phần tử lớn nhất trong một danh sách bất kỳ:

Code
// Hàm lấy phần tử lớn nhất, sử dụng kiểu tổng quát T
// Ràng buộc T phải implement trait PartialOrd để có thể so sánh lớn/nhỏ
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    println!("Số lớn nhất là: {}", largest(&number_list));

    let char_list = vec!['y', 'm', 'a', 'q'];
    println!("Ký tự lớn nhất là: {}", largest(&char_list));
}

B. Generics trong Struct và Enum

Bạn cũng có thể dùng Generics bên trong Struct hoặc Enum để tạo ra các cấu trúc dữ liệu đa năng. Ví dụ kinh điển chính là kiểu Option<T> hay Result<T, E> mà chúng ta đã dùng ở các bài trước:

Code
// Định nghĩa một điểm tọa độ có thể dùng kiểu số nguyên hoặc số thực
struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer_point = Point { x: 5, y: 10 };       // T là i32
    let float_point = Point { x: 1.0, y: 4.5 };     // T là f64
}

🤝 2. Traits (Định nghĩa hành vi chung)

Nếu Generics giúp bạn viết code cho mọi kiểu dữ liệu, thì Trait lại giúp bạn chỉ định rõ ràng rằng: "Kiểu dữ liệu đó phải có những hành vi (phương thức) cụ thể nào". Trait tương tự như Interface trong các ngôn ngữ lập trình hướng đối tượng.

A. Khai báo và Implement một Trait

Giả sử chúng ta đang xây dựng một ứng dụng tin tức hoặc mạng xã hội, nơi cả Article (Bài viết) và Tweet đều cần có tính năng tóm tắt nội dung (summary):

Code
// 1. Định nghĩa Trait chứa bản thiết kế hành vi
pub trait Summary {
    // Phương thức bắt buộc các struct triển khai phải viết logic riêng
    fn summarize(&self) -> String;

    // Hoặc cung cấp một hành vi mặc định (default implementation)
    fn summarize_author(&self) -> String {
        String::from("(Đọc thêm từ tác giả...)")
    }
}

// 2. Định nghĩa Struct Article
pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

// Implement Trait Summary cho NewsArticle
impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, bởi {} ({})", self.headline, self.author, self.location)
    }
}

// 3. Định nghĩa Struct Tweet
pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

// Implement Trait Summary cho Tweet
impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

B. Sử dụng Trait làm tham số cho hàm (Trait Bounds)

Điểm mạnh nhất của Trait là bạn có thể viết các hàm nhận vào bất kỳ kiểu dữ liệu nào miễn là nó cài đặt (implement) một Trait cụ thể:

Code
// Hàm nhận vào bất kỳ đối tượng nào có implement trait Summary
fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

// Hoặc sử dụng cú pháp Trait Bound tường minh hơn (rất hữu ích khi hàm phức tạp)
fn notify_generic<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

🛡️ 3. Ràng buộc đa điều kiện và mệnh đề where

Đôi khi một hàm của bạn yêu cầu kiểu dữ liệu phải thỏa mãn nhiều Trait cùng lúc (ví dụ vừa phải in được ra màn hình Display, vừa phải so sánh được Clone hoặc PartialOrd).

Nếu viết theo kiểu thông thường, danh sách tham số sẽ trở nên khó đọc:

Code
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) {}

Rust cung cấp mệnh đề where đặt ở cuối định nghĩa hàm giúp mã nguồn trở nên cực kỳ thoáng đãng và dễ bảo trì:

Code
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    // Logic xử lý ở đây
    0
}

Thông qua GenericsTraits, bạn đã nắm giữ công cụ cốt lõi để viết mã nguồn dạng thư viện (library) cực kỳ linh hoạt, sạch sẽ và chuẩn phong cách lập trình hệ thống hiện đại.

Ở bài viết tiếp theo (Bài 10), chúng ta sẽ khám phá các Collections thông dụng trong Rust (Vector, HashMap, String nâng cao) để quản lý tập dữ liệu lớn trong thực tế!

Bình luận

Đăng nhập để để lại bình luận.
Chưa có bình luận nào cho bài viết này.

Bài viết liên quan