Generic trong Rust
Bạn đã dùng generic từ lâu dù có thể chưa gọi tên nó: Vec<i32> và Option<T> đều dùng generic. Trang này hướng dẫn bạn tự viết hàm và struct generic của riêng mình.
Vấn đề mà generic giải quyết
Phần tiêu đề “Vấn đề mà generic giải quyết”Giả sử bạn muốn một hàm trả về số lớn nhất trong một danh sách số, cho cả i32 lẫn f64. Không có generic, bạn phải viết hai lần:
fn largest_i32(numbers: &[i32]) -> i32 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn largest_f64(numbers: &[f64]) -> f64 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}Cùng một logic, sao chép lại cho từng kiểu. Generic cho phép bạn chỉ viết một lần.
Một hàm generic
Phần tiêu đề “Một hàm generic”fn largest<T: PartialOrd + Copy>(numbers: &[T]) -> T { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn main() { let integers = [10, 25, 3, 47, 8]; let floats = [1.5, 2.8, 0.3];
println!("{}", largest(&integers)); println!("{}", largest(&floats));}Kết quả:
472.8Đọc <T: PartialOrd + Copy> là “với mọi kiểu T, miễn là nó so sánh được (PartialOrd) và sao chép được (Copy)”. T là một kiểu giữ chỗ: Rust sinh ra một phiên bản của hàm cho đúng kiểu cụ thể mà bạn gọi (i32, f64, v.v.).
Một struct generic
Phần tiêu đề “Một struct generic”Struct cũng có thể là generic:
struct Pair<T> { first: T, second: T,}
fn main() { let numbers = Pair { first: 5, second: 10 }; let words = Pair { first: "xin chào", second: "thế giới" };
println!("{} {}", numbers.first, numbers.second); println!("{} {}", words.first, words.second);}Kết quả:
5 10xin chào thế giớiMột định nghĩa Pair<T> dùng được cho số, chữ hay bất kỳ kiểu nào khác: numbers là Pair<i32> còn words là Pair<&str>, nhưng bạn chỉ viết struct đúng một lần.
Điều này có tốn gì lúc chạy không?
Phần tiêu đề “Điều này có tốn gì lúc chạy không?”Không. Rust sinh ra một phiên bản riêng, chuyên biệt của hàm hoặc struct cho từng kiểu cụ thể mà bạn thật sự dùng (gọi là monomorphization, đơn hình hoá). Đến lúc chương trình chạy, không còn chút code generic nào: chỉ còn những hàm bình thường với kiểu đầy đủ, nhanh y như khi bạn tự viết tay từng hàm.
Phương thức trên struct generic
Phần tiêu đề “Phương thức trên struct generic”Bạn có thể thêm phương thức cho struct generic bằng một khối impl, giống như ở trang Struct. Điểm mới duy nhất là chính impl cũng cần <T> của riêng nó:
struct Pair<T> { first: T, second: T,}
impl<T> Pair<T> { fn new(first: T, second: T) -> Self { Pair { first, second } }
fn swap(self) -> Pair<T> { Pair { first: self.second, second: self.first } }}
impl Pair<f64> { fn average(&self) -> f64 { (self.first + self.second) / 2.0 }}
fn main() { let words = Pair::new("trái", "phải").swap(); println!("{} {}", words.first, words.second);
let scores = Pair::new(7.0, 9.0); println!("{}", scores.average());}Kết quả:
phải trái8Ở đây có hai loại khối impl:
impl<T> Pair<T>nghĩa là “các phương thức này tồn tại choPaircủa mọi kiểu”. Hãy đọc<T>đầu tiên là “với mọi kiểuT”. Thiếu nó, Rust sẽ nghĩTlà tên của một kiểu có thật và báo lỗi vì không tìm thấy.impl Pair<f64>không có<T>. Các phương thức của nó chỉ tồn tại choPair<f64>. Đó là lý doaveragedùng được+và/ 2.0. Thử gọiwords.average()xem, trình biên dịch sẽ báo không có phương thức đó choPair<&str>.
Một struct cũng có thể có nhiều tham số kiểu, như struct Pair<T, U> { first: T, second: U }, khi hai trường có thể khác kiểu nhau. Và khi ràng buộc trait dài ra, bạn có thể chuyển chúng vào mệnh đề where để chữ ký hàm dễ đọc hơn. Phần đó có ở trang Trait nâng cao.
Luyện tập
Phần tiêu đề “Luyện tập”1. Hàm smallest generic
Phần tiêu đề “1. Hàm smallest generic”Hãy viết fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T trả về phần tử nhỏ nhất. Thử với [8, 3, 5] và với các ký tự ['r', 'u', 's', 't']. Gợi ý: đó chính là hàm largest trên trang này với một phép so sánh bị đảo lại.
Xem lời giải
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T { let mut smallest = items[0]; for &item in items { if item < smallest { smallest = item; } } smallest}
fn main() { println!("{}", smallest(&[8, 3, 5])); println!("{}", smallest(&['r', 'u', 's', 't']));}Kết quả:
3r2. Nửa lớn hơn của một Pair
Phần tiêu đề “2. Nửa lớn hơn của một Pair”Hãy thêm phương thức larger(&self) -> &T cho Pair<T>, trả về tham chiếu tới trường nào lớn hơn. Thử với một Pair số và một Pair từ. Gợi ý: so sánh cần PartialOrd, nên hãy đặt ràng buộc ngay trên impl: impl<T: PartialOrd> Pair<T>.
Xem lời giải
struct Pair<T> { first: T, second: T,}
impl<T: PartialOrd> Pair<T> { fn larger(&self) -> &T { if self.first > self.second { &self.first } else { &self.second } }}
fn main() { let numbers = Pair { first: 3, second: 9 }; let words = Pair { first: "pear", second: "apple" };
println!("{}", numbers.larger()); println!("{}", words.larger());}Kết quả:
9pearHai từ "pear" (lê) và "apple" (táo) được giữ tiếng Anh vì chuỗi được so sánh theo thứ tự chữ cái: "pear" đứng sau "apple" nên được coi là “lớn hơn”.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Generic cho phép bạn viết một hàm hoặc struct dùng được với nhiều kiểu, thay vì lặp lại code.
<T>là kiểu giữ chỗ; ràng buộc trait (T: PartialOrd) giới hạnTđược phép là gì.impl<T> Pair<T>thêm phương thức cho mọiT;impl Pair<f64>chỉ thêm phương thức cho một kiểu cụ thể.- Code generic được biên dịch thành các phiên bản chuyên biệt cho từng kiểu, không tốn hiệu năng lúc chạy.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Generic giải quyết vấn đề gì?
2. Trong fn largest<T: PartialOrd + Copy>(...), PartialOrd + Copy có tác dụng gì?
3. Dùng generic có tốn gì lúc chạy so với code viết tay không?
4. Một định nghĩa struct Pair<T> duy nhất cho phép bạn làm gì?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- Generic Data Types — The Rust Book (tiếng Anh): generic trong hàm, struct, enum và phương thức, kể cả ví dụ
largest. - Generics — Rust by Example (tiếng Anh): ví dụ ngắn chạy được về hàm, struct generic và cú pháp của chúng.
- Implementation — Rust by Example (tiếng Anh): thêm về khối
impl<T>và cài đặt phương thức cho một kiểu cụ thể. - Bounds — Rust by Example (tiếng Anh): ràng buộc trait giới hạn kiểu generic thế nào, và vì sao bạn cần chúng.
- Generic parameters — The Rust Reference (tiếng Anh): các quy tắc chính xác, khi bạn muốn biết đầy đủ chi tiết.