Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generic, Trait, Lifetime

Ba khái niệm của chương này giải quyết ba vấn đề khác nhau nhưng đều xoay quanh việc viết code dùng lại được: generic tránh lặp code xử lý cùng logic trên nhiều kiểu cụ thể, trait định nghĩa hành vi chung mà nhiều kiểu khác nhau có thể cùng implement, lifetime đảm bảo mọi tham chiếu dùng lại vẫn luôn hợp lệ. Cả ba đều được trình biên dịch giải quyết tại thời điểm biên dịch — không cái nào có chi phí lúc chạy.

Generic

Xét hai hàm gần như giống hệt nhau, chỉ khác kiểu tham số:

fn so_lon_nhat_i32(danh_sach: &[i32]) -> &i32 {
    let mut lon_nhat = &danh_sach[0];
    for so in danh_sach {
        if so > lon_nhat {
            lon_nhat = so;
        }
    }
    lon_nhat
}

fn so_lon_nhat_char(danh_sach: &[char]) -> &char {
    let mut lon_nhat = &danh_sach[0];
    for ky_tu in danh_sach {
        if ky_tu > lon_nhat {
            lon_nhat = ky_tu;
        }
    }
    lon_nhat
}

fn main() {
    let so = vec![34, 50, 25, 100, 65];
    println!("{}", so_lon_nhat_i32(&so));
}

Phần thân hai hàm giống hệt nhau — chỉ kiểu i32/char khác. Generic cho viết một hàm duy nhất, tham số hoá theo kiểu:

fn so_lon_nhat<T: PartialOrd + Copy>(danh_sach: &[T]) -> T {
    let mut lon_nhat = danh_sach[0];
    for &item in danh_sach {
        if item > lon_nhat {
            lon_nhat = item;
        }
    }
    lon_nhat
}

fn main() {
    let so = vec![34, 50, 25, 100, 65];
    println!("{}", so_lon_nhat(&so));

    let ky_tu = vec!['y', 'm', 'a', 'q'];
    println!("{}", so_lon_nhat(&ky_tu));
}

T là tham số kiểu, đứng trong <> ngay sau tên hàm — quy ước đặt tên một chữ hoa (T, U, …) khi không có ý nghĩa cụ thể hơn tên đó có thể gợi ra. T: PartialOrd + Copyràng buộc trait (trait bound, nói kỹ ở phần Trait ngay sau đây): không phải T nhận bất kỳ kiểu nào, chỉ những kiểu implement PartialOrd (so sánh được bằng >) và Copy (copy được thay vì move khi gán) mới hợp lệ — thiếu ràng buộc, so > lon_nhat không biên dịch được vì trình biên dịch không có gì đảm bảo T hỗ trợ phép so sánh đó.

Generic trên struct và enum

struct Diem<T> {
    x: T,
    y: T,
}

fn main() {
    let nguyen = Diem { x: 5, y: 10 };
    let thuc = Diem { x: 1.0, y: 4.0 };

    println!("{} {}", nguyen.x, thuc.y);
}

Cả xy cùng dùng một T — một instance phải có cả hai field cùng kiểu, viết Diem { x: 5, y: 4.0 } không biên dịch được vì 5i32 còn 4.0f64. Cần hai field khác kiểu độc lập, khai báo hai tham số: struct Diem<T, U> { x: T, y: U }. Option<T>Result<T, E> đã gặp ở các chương trước chính là enum generic — không có gì đặc biệt hơn cách viết ở đây.

Method trên kiểu generic

struct Diem<T> {
    x: T,
    y: T,
}

impl<T> Diem<T> {
    fn lay_x(&self) -> &T {
        &self.x
    }
}

impl Diem<f64> {
    fn khoang_cach_tu_goc(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p = Diem { x: 3.0, y: 4.0 };
    println!("{} {}", p.lay_x(), p.khoang_cach_tu_goc());
}

impl<T> Diem<T> khai báo method cho mọi T. impl Diem<f64> (không có <T> sau impl) chỉ khai báo method cho riêng trường hợp Tf64 — hữu ích khi một phép tính (ở đây là khoang_cach_tu_goc, cần sqrt) chỉ có ý nghĩa với một kiểu cụ thể, không tổng quát hoá được cho mọi T.

Generic không có chi phí lúc chạy

Trình biên dịch monomorphize (đơn hình hoá) code generic: với mỗi kiểu cụ thể được dùng thật ở đâu đó trong code, trình biên dịch tự sinh ra một bản sao code đã thay T bằng kiểu cụ thể đó, y hệt như thể người viết code tự tay viết so_lon_nhat_i32so_lon_nhat_char riêng biệt. Nhờ vậy dùng generic không hề chậm hơn viết tay từng phiên bản cụ thể — toàn bộ phần “tổng quát hoá” chỉ tồn tại lúc biên dịch, biến mất hoàn toàn trong binary cuối cùng.