Bỏ qua để đến nội dung

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.

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.

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ả:

47
2.8
▶ Tự chạy thử

Đọ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.).

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 10
xin chào thế giới
▶ Tự chạy thử

Mộ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.

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.

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ái
8
▶ Tự chạy thử

Ở đâ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 cho Pair của mọi kiểu”. Hãy đọc <T> đầu tiên là “với mọi kiểu T”. Thiếu nó, Rust sẽ nghĩ T là 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 cho Pair<f64>. Đó là lý do average dùng được + và / 2.0. Thử gọi words.average() xem, trình biên dịch sẽ báo không có phương thức đó cho Pair<&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.

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ả:

3
r
▶ Tự chạy thử

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ả:

9
pear
▶ Tự chạy thử

Hai 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”.

  • 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ạn T được phép là gì.
  • impl<T> Pair<T> thêm phương thức cho mọi T; 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

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