Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu cú pháp
fn f() -> impl Traitvà ý nghĩa "opaque return type" — caller chỉ thấy trait, không thấy concrete type. - Phân biệt được
impl Trait(static dispatch, một concrete type) vàdyn Trait(dynamic dispatch, nhiều concrete type qua vtable). - Đọc được lỗi khi hàm cố trả về hai concrete type khác nhau qua
impl Traitvà biết cách khắc phục. - Trả về được closure và iterator chain không tên gọi, vốn không thể viết kiểu cụ thể.
- Thêm lifetime
'_khi opaque type capture borrow của input. - Hiểu mối liên hệ giữa
async fnvàimpl Future— phép biến đổi compiler thực hiện ngầm.
impl Trait Return Type Là Gì
Ở các bài trước, mỗi hàm khai báo return type là một concrete type cụ thể: -> i32, -> String, -> Vec<u8>. impl Trait ở vị trí return thay vì nêu tên type, chỉ nêu trait mà type đó phải implement:
fn make_iter() -> impl Iterator<Item = i32> {
vec![1, 2, 3].into_iter()
}
fn main() {
let it = make_iter();
for x in it {
println!("{x}");
}
}
Compiler nội bộ biết concrete type là std::vec::IntoIter<i32>, nhưng signature chỉ hiện impl Iterator<Item = i32>. Caller không gọi được method của IntoIter riêng (ví dụ .as_slice()), chỉ gọi được method có trên trait Iterator. Đây gọi là opaque return type hay viết tắt RPIT (Return Position Impl Trait).
Lợi ích thực tế: thay đổi nội bộ (đổi vec![...].into_iter() sang (0..3)) mà signature không đổi, caller không cần sửa code. Đồng thời concrete type vẫn cố định ở compile-time nên không có overhead heap hay vtable — performance ngang với hàm trả type cụ thể.
Khác Với dyn Trait
Hai cú pháp dễ nhầm vì cùng nói "hàm trả về một thứ impl Trait":
-> impl Iterator<Item = i32>: opaque static dispatch. Mỗi hàm có đúng một concrete type ẩn. Compiler monomorph hoá: mỗi call site biết chính xác concrete type, gọi method qua địa chỉ tĩnh.-> Box<dyn Iterator<Item = i32>>: dynamic dispatch. Concrete type được wrap qua trait object, lookup method qua vtable mỗi lần gọi. Hàm có thể trả nhiều concrete type khác nhau qua các nhánh.
Cùng signature ý nghĩa với caller, nhưng trade-off khác hẳn. impl Trait nhanh hơn (không vtable, inline được) nhưng "cứng" hơn (một concrete per hàm). dyn Trait linh hoạt hơn (nhiều concrete) nhưng có overhead và bắt buộc đi qua con trỏ (Box, &, Rc...).
Quy tắc thực dụng: nếu chỉ có một concrete type và bạn chỉ muốn giấu nó đi, chọn impl Trait. Nếu thật sự cần đa hình runtime (collection chứa nhiều type), chọn Box<dyn Trait>.
Restriction: Một Concrete Type Per Hàm
Đây là chỗ người mới hay vấp. impl Trait ở return position cho phép giấu concrete type, nhưng hàm vẫn phải trả đúng một concrete cho mọi nhánh. Code dưới đây nhìn hợp lý nhưng compile lỗi:
fn either(b: bool) -> impl Iterator<Item = i32> {
if b {
vec![1, 2, 3].into_iter() // std::vec::IntoIter<i32>
} else {
(0..10).into_iter() // std::ops::Range<i32>
}
}
Compiler báo error[E0308]: `if` and `else` have incompatible types: nhánh if trả IntoIter<i32>, nhánh else trả Range<i32>. Dù cả hai cùng impl Iterator<Item = i32>, opaque type chỉ "đại diện" cho đúng một concrete — không thể đồng thời là cả hai.
Cách khắc phục: nếu thực sự cần đa hình runtime, đổi sang Box<dyn Iterator<Item = i32>>. Nếu chỉ cần thống nhất type, dùng Iterator::chain hoặc .collect() về cùng Vec rồi .into_iter(). Trong nhiều trường hợp, thiết kế lại để mỗi hàm chỉ trả một nguồn duy nhất sẽ sạch hơn.
Use Case: Return Closure
Mỗi closure có một concrete type duy nhất do compiler sinh tự động, không có tên bạn có thể viết ra. Vì vậy hàm muốn trả closure trước khi có impl Trait phải bọc qua Box<dyn Fn>. Có impl Trait, chuyện đơn giản hẳn:
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
fn main() {
let add5 = make_adder(5);
println!("{}", add5(10)); // 15
println!("{}", add5(20)); // 25
}
Closure move |y| x + y có concrete type ẩn (compiler sinh struct chứa field x và impl Fn(i32) -> i32). Caller chỉ thấy impl Fn(i32) -> i32 và gọi như hàm. So với Box<dyn Fn(i32) -> i32>: không allocation heap, không vtable, gọi trực tiếp — đặc biệt quan trọng với code hot path.
Lưu ý move cần thiết khi closure capture biến by value — closure phải sở hữu x để sống được sau khi hàm make_adder kết thúc.
Use Case: Iterator Chain
Iterator combinator trong Rust trả về struct generic lồng nhau với tên dài kinh hoàng. Một chain ngắn:
data.iter().filter(|&&x| x > 0).copied()
có concrete type là std::iter::Copied<std::iter::Filter<std::slice::Iter<'_, i32>, <closure>>>. Vô phương viết tay nếu có closure (closure không có tên). impl Trait giải quyết gọn:
fn process(data: &[i32]) -> impl Iterator<Item = i32> + '_ {
data.iter().filter(|&&x| x > 0).copied()
}
fn main() {
let v = vec![-1, 2, -3, 4, 5];
let sum: i32 = process(&v).sum();
println!("{sum}"); // 11
}
Hàm process trả opaque iterator, caller chỉ biết "có thể duyệt ra i32". Đây là idiom rất phổ biến: helper function trả iterator chain để chia nhỏ logic xử lý mà không tốn allocation. Lưu ý + '_ cuối signature — sẽ giải thích ở bước kế.
Lifetime Trong impl Trait
Khi opaque return type chứa borrow của input, phải nói rõ nó "sống chung" với reference đó. Phần + '_ ở bước 6 chính là việc đó:
fn process<'a>(data: &'a [i32]) -> impl Iterator<Item = i32> + 'a {
data.iter().filter(|&&x| x > 0).copied()
}
// Cú pháp ngắn gọn — '_ tự ánh xạ về lifetime của borrow input duy nhất:
fn process2(data: &[i32]) -> impl Iterator<Item = i32> + '_ {
data.iter().filter(|&&x| x > 0).copied()
}
Lý do: iterator trả về giữ std::slice::Iter<'a, i32> bên trong, mà Iter borrow từ slice gốc. Nếu không khai báo lifetime, compiler báo error[E0700]: hidden type for impl Trait captures lifetime. '_ là cú pháp anonymous lifetime — bảo "iterator sống không quá lifetime của input".
Rust 2024 edition đổi rule capture cho RPIT: tự động capture mọi generic lifetime của hàm, kể cả khi không viết + '_. Code Rust 2024 ở trên có thể bỏ + '_ vẫn compile. Edition trước (2021/2018) cần ghi rõ. Tham khảo guide trong node_modules/next/dist/docs/ hoặc release note Rust 2024 để xem chi tiết — và luôn check edition trong Cargo.toml trước khi viết.
+ Bound Multiple Trait
Đôi khi opaque type cần thoả nhiều trait — ví dụ vừa là Iterator vừa Clone để caller copy hoặc retry. Dùng dấu + để nối:
fn cloneable_iter() -> impl Iterator<Item = i32> + Clone {
(1..=5)
}
fn main() {
let it = cloneable_iter();
let it2 = it.clone(); // OK vì Range<i32> impl Clone
let sum: i32 = it.sum();
let prod: i32 = it2.product();
println!("sum={sum} prod={prod}");
}
Caller được phép gọi method từ cả hai trait. Concrete type Range<i32> phải thật sự impl đủ Iterator và Clone — nếu trả về vec![...].into_iter() (IntoIter impl Clone nên vẫn OK) hay closure (closure thường không impl Clone trừ khi capture toàn Copy data) sẽ compile lỗi.
Common bound thêm: + Send, + Sync, + 'static — quan trọng với async và thread. Sẽ gặp lại ở bài về async.
async fn Return impl Future
async fn không phải feature đứng một mình — nó là sugar được desugar về impl Future. Hai signature dưới đây tương đương:
// Cú pháp async fn (cú pháp viết)
async fn fetch(url: &str) -> String {
/* ... await something ... */
String::from("body")
}
// Sau khi compiler desugar (cú pháp tương đương)
fn fetch<'a>(url: &'a str) -> impl std::future::Future<Output = String> + 'a {
async move {
/* ... await something ... */
String::from("body")
}
}
Compiler tự sinh state machine impl Future<Output = String> — concrete type của state machine đó không có tên (giống closure), nên chỉ có thể trả qua opaque impl Future. Đây là lý do RPIT là feature nền tảng cho async Rust: không có impl Trait, mỗi async fn phải trả Pin<Box<dyn Future<Output = T>>> với heap allocation và vtable cho mỗi call.
Hệ quả: hiểu được rule lifetime / capture của RPIT giúp đọc lỗi async dễ hơn nhiều — phần lớn lỗi async khó là lỗi RPIT đội lốt. Nhóm async sẽ xoáy vào đề tài này.
Tổng Kết
fn f() -> impl Traitlà opaque return type: caller chỉ thấy trait, compiler giữ concrete type ẩn. Static dispatch, không heap, không vtable.- Khác
Box<dyn Trait>:impl Trait= một concrete + static dispatch;dyn Trait= nhiều concrete + dynamic dispatch. - Restriction quan trọng: hàm chỉ được trả đúng một concrete cho mọi nhánh —
if-elsetrả hai iterator khác type sẽ compile lỗi E0308. - Use case kinh điển: trả closure (concrete type không có tên), trả iterator chain dài, helper trả lazy sequence.
- Khi opaque type chứa borrow của input, cần lifetime
+ '_(Rust 2018/2021) — Rust 2024 tự động capture lifetime. - Dùng
+để bound nhiều trait:impl Iterator<Item = i32> + Clone + Send + 'static. async fnchính là sugar chofn ... -> impl Future<Output = T>— RPIT là nền tảng của async Rust.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Viết hàm
fn powers_of(base: i32, n: usize) -> impl Iterator<Item = i32>trả iterator sinhbase^0, base^1, ..., base^(n-1). Test bằngpowers_of(2, 5).collect::<Vec<_>>(). - Hàm
either(b: bool) -> impl Iterator<Item = i32>ở bước 4 vì sao compile lỗi? Đề xuất hai cách sửa: (a) dùngBox<dyn Iterator>, (b) thống nhất type bằngcollect. - Viết
fn make_multiplier(x: i32) -> impl Fn(i32) -> i32trả closure nhân vớix. Bỏmovera khỏi closure — lỗi gì xảy ra? Vì sao? - Hàm
fn evens(data: &[i32]) -> impl Iterator<Item = i32>không có+ '_compile lỗi trên Rust 2021. Giải thích vì sao, và cho biết edition 2024 vì sao không cần. - Cho
async fn task(n: u32) -> u32 { n * 2 }. Viết lại bằngfn task(n: u32) -> impl Future<Output = u32>dùng blockasync move { ... }. Hai cách có khác nhau khi gọi không?
Đáp án
fn powers_of(base: i32, n: usize) -> impl Iterator<Item = i32> { (0..n).map(move |i| base.pow(i as u32)) }.maptrảMap<Range<usize>, _>implIterator<Item = i32>— concrete type ẩn, caller chỉ thấyimpl Iterator.- Hai nhánh trả hai concrete type khác nhau (
IntoItervsRange),impl Traitchỉ đại diện được đúng một concrete. Sửa: (a)fn either(b: bool) -> Box<dyn Iterator<Item = i32>> { if b { Box::new(vec![1,2,3].into_iter()) } else { Box::new(0..10) } }; (b)let v: Vec<i32> = if b { vec![1,2,3] } else { (0..10).collect() }; v.into_iter()— cả hai cùng trảIntoIter<i32>. - Lỗi
closure may outlive the current function, but it borrows `x`: closure không cómovesẽ borrowxbằng reference, nhưngxlà local biến mất khi hàm return → closure sẽ dangling.moveép closure sở hữux, sống được sau khi hàm thoát. - Iterator trả về capture
std::slice::Iter<'a, i32>bên trong, lifetime'ađến từ inputdata. Rust 2018/2021 không tự suy ra lifetime đó vào opaque type, phải khai báo+ '_. Rust 2024 đổi rule: RPIT mặc định capture mọi generic lifetime của hàm, nên+ '_không bắt buộc. fn task(n: u32) -> impl Future<Output = u32> { async move { n * 2 } }. Hai cách tương đương khi gọi — đều phải.awaitđể lấyu32. Khác duy nhất là cú pháp viết; compiler sinh ra cùng state machine.
Bài Tiếp Theo
Bài 172: impl Trait Trong Argument — đối xứng với bài này, đặt impl Trait ở vị trí argument: fn print(item: impl Display). Sẽ thấy ở argument position, impl Trait chỉ là syntactic sugar cho generic fn print<T: Display>(item: T), ý nghĩa hoàn toàn khác với return position (vốn là opaque type). Bài đi qua trade-off, khi nào dùng cú pháp ngắn, khi nào nên giữ generic tường minh.
