Danh sách bài viết

Bài 171: impl Trait Trong Return Type

Bài 171 của series Rust Cơ Bản — impl Trait ở vị trí return type cho phép hàm trả về một giá trị nhưng giấu concrete type của nó khỏi caller mà không trả giá heap allocation hay vtable lookup. Cú pháp fn make_iter() -> impl Iterator<Item = i32> nói "hàm trả về một thứ impl Iterator, nhưng không tiết lộ concrete type là gì". Caller chỉ gọi được method của Iterator, compiler vẫn biết chính xác concrete type và sinh static dispatch tối ưu. Khác Box<dyn Trait>: impl Trait là một concrete type per hàm — if-else trả hai iterator khác nhau sẽ compile lỗi E0308. Use case kinh điển: trả closure, iterator chain dài, và async fn — async fn foo() -> X chính là sugar cho fn foo() -> impl Future<Output = X>. Bài đi qua cú pháp, khác biệt với dyn Trait, restriction một concrete, lifetime '_ khi capture borrow, kết hợp nhiều trait với +, và quan hệ với async fn.

09/06/2026
10 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu cú pháp fn f() -> impl Trait và ý 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 Trait và 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 fnimpl Future — phép biến đổi compiler thực hiện ngầm.
2

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ể.

3

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

4

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.

5

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.

6

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ế.

7

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.

8

+ 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 đủ IteratorClone — 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.

9

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.

10

Tổng Kết

  • fn f() -> impl Traitopaque 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-else trả 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 fn chính là sugar cho fn ... -> impl Future<Output = T> — RPIT là nền tảng của async Rust.
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Viết hàm fn powers_of(base: i32, n: usize) -> impl Iterator<Item = i32> trả iterator sinh base^0, base^1, ..., base^(n-1). Test bằng powers_of(2, 5).collect::<Vec<_>>().
  2. 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ùng Box<dyn Iterator>, (b) thống nhất type bằng collect.
  3. Viết fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 trả closure nhân với x. Bỏ move ra khỏi closure — lỗi gì xảy ra? Vì sao?
  4. 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.
  5. Cho async fn task(n: u32) -> u32 { n * 2 }. Viết lại bằng fn task(n: u32) -> impl Future<Output = u32> dùng block async move { ... }. Hai cách có khác nhau khi gọi không?
Đáp án
  1. fn powers_of(base: i32, n: usize) -> impl Iterator<Item = i32> { (0..n).map(move |i| base.pow(i as u32)) }. map trả Map<Range<usize>, _> impl Iterator<Item = i32> — concrete type ẩn, caller chỉ thấy impl Iterator.
  2. Hai nhánh trả hai concrete type khác nhau (IntoIter vs Range), impl Trait chỉ đạ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>.
  3. Lỗi closure may outlive the current function, but it borrows `x`: closure không có move sẽ borrow x bằng reference, nhưng x là local biến mất khi hàm return → closure sẽ dangling. move ép closure sở hữu x, sống được sau khi hàm thoát.
  4. Iterator trả về capture std::slice::Iter<'a, i32> bên trong, lifetime 'a đến từ input data. 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.
  5. 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ấy u32. Khác duy nhất là cú pháp viết; compiler sinh ra cùng state machine.
12

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.