
Vấn đề với async fn trong trait trước Rust 1.75
Lập trình bất đồng bộ (async) đã trở thành phần không thể thiếu trong Rust hiện đại. Tuy nhiên, việc định nghĩa async fn trực tiếp trong trait từng bị cấm vì Future trả về không có kích thước cố định (unsized). Điều này khiến compiler không thể xác định kích thước vtable khi dùng dyn, gây khó khăn cho việc viết code linh hoạt và an toàn kiểu.
Trước đây, lập trình viên phải dùng dyn Future + Box hoặc macro như async-trait, vừa phát sinh chi phí heap allocation, vừa mất khả năng tối ưu hóa của compiler. Giải pháp này không hoàn toàn “Rust-like” và có thể gây runtime overhead không mong muốn.

Giải pháp: async fn trong trait (stabilized Rust 1.75)
Từ Rust 1.75, async fn trong trait đã được chính thức ổn định. Khi bạn viết async fn trong trait, compiler ngầm tạo ra một impl Future có lifetime liên kết với &self hoặc &mut self. Điều này giải quyết vấn đề unsized return type bằng cách sử dụng kỹ thuật Generic Associated Types (GAT).
Nguyên tắc cốt lõi: mỗi lệnh gọi async fn trong trait trả về một kiểu Future khác nhau, nhưng compiler đảm bảo tất cả chúng triển khai cùng một trait, cho phép viết code generic an toàn kiểu mà không cần Box.
Ví dụ code cơ bản
// async fn in trait - hoạt động từ Rust 1.75
trait AsyncService {
async fn fetch(&self, id: u32) -> Result<String, Error>;
}
struct DatabaseService;
impl AsyncService for DatabaseService {
async fn fetch(&self, id: u32) -> Result<String, Error> {
// logic fetch async
Ok(format!("record_{}", id))
}
}
async fn run(service: &impl AsyncService) {
let data = service.fetch(1).await;
println!("{:?}", data);
}

Generic Associated Types (GAT) và vai trò trong async
GAT là tính năng cho phép trait định nghĩa associated type có generic parameter. Đây là thành phần then chốt giúp async fn trong trait hoạt động.
Thay vì yêu cầu một kiểu duy nhất cho Future, GAT cho phép trait trả về Future với các tham số khác nhau, phụ thuộc vào context gọi:
// GAT dạng tổng quát
trait Iterator {
type Item<'a> where Self: 'a;
}
// Trong async, GAT biểu diễn ngầm:
// Future<'a, Output = T> trong trait method
So sánh GAT với giải pháp cũ
| Tiêu chí | async-trait crate | async fn in trait + GAT |
|---|---|---|
| Heap allocation | Có (Box::pin) |
Không |
| Type inference | Kém do impl Trait ẩn |
Tốt, giữ nguyên type |
| Performance | Thêm indirection | Zero-cost abstraction |
| Stability | Deprecated hướng tới | Stable từ 1.75 |

Best practices khi dùng async fn trong trait
Khi chuyển từ cũ sang mới, lập trình viên cần lưu ý một số quy tắc:
- Receiver: Ưu tiên
&selfhoặc&mut self. Tránhselfbằng giá trị trừ khi thực sự cần. - Dyn compatibility:
AsyncFn,AsyncFnMut,AsyncFnOncekhông dyn-compatible. Nếu cần trait object, vẫn phải dùngBoxhoặc enum dispatch. - Bounds: Có thể thêm bound
where Self: Sizedcho method không cần dispatch. - Edge cases: Các lifetime phức tạp có thể cần explicit
use<'a>clause.
// Kết hợp async fn với GAT pattern
trait Repository {
type Cursor<'a>: Iterator<Item = &'a Row> where Self: 'a;
async fn query(&self, sql: &str) -> Result<Self::Cursor<'_>, Error>;
// Default method với async fn
async fn find_all(&self) -> Result<Vec<Row>, Error> {
let mut cursor = self.query("SELECT *").await?;
let mut results = Vec::new();
for row in cursor {
results.push(row);
}
Ok(results)
}
}
Khi nào nên dùng GAT và async fn in trait?
GAT phù hợp khi bạn cần một trait trừu tượng với method bất đồng bộ, ví dụ: repository pattern, service layer, plugin system. Bạn có thể viết code generic mà không tốn chi phí heap allocation, vẫn giữ được khả năng tối ưu hóa tự nhiên của Rust.
Tuy nhiên, nếu hệ thống của bạn yêu cầu dyn dispatch với async method, GAT chưa phải là giải pháp tối ưu. Khi đó, enum-based dispatch hoặc Box vẫn là lựa chọn thực tế hơn.
Kết luận
Stabilization của async fn trong trait đánh dấu bước tiến lớn của Rust. Kết hợp với GAT, lập trình viên có thể viết code bất đồng bộ trừu tượng, an toàn kiểu và hiệu quả. Việc hiểu rõ cơ chế này giúp tận dụng tối đa sức mạnh của Rust mà không phải hy sinh tính linh hoạt.
