本文涵蓋為什麼 Rust trait 不支援 async fn、async_trait crate 如何解決這個問題、它的代價是什麼,以及 Rust 1.75 之後的原生支援現況。
問題:async fn 不能直接用在 trait 裡
如果你第一次在 Rust 寫 async 相關的 trait,很可能會直覺地這樣寫:
trait MyTrait {
async fn do_something(&self) -> String; // ❌ 編譯錯誤
}但這在穩定版 Rust(1.75 之前)是不允許的。為什麼?
根本原因:impl Trait 與 dyn Trait 的衝突
async fn 是語法糖,它會被編譯器展開成回傳 impl Future 的普通函式:
// 你寫的:
async fn do_something(&self) -> String { ... }
// 編譯器看到的:
fn do_something(&self) -> impl Future<Output = String> { ... }問題出在 trait 裡。每個實作 trait 的型別,其 do_something 都會回傳一個不同的具體 Future 型別:
impl MyTrait for StructA {
// 回傳型別是某個只有編譯器知道名字的 Future_A
async fn do_something(&self) -> String { ... }
}
impl MyTrait for StructB {
// 回傳型別是另一個 Future_B,跟 Future_A 完全不同
async fn do_something(&self) -> String { ... }
}這讓 trait 無法做到 object-safe(也就是無法用 dyn MyTrait),因為動態派發需要在執行期才知道要呼叫哪個實作,但每個實作的回傳型別大小不同,vtable 根本無法表達。
解法:async_trait crate
async_trait 是由 David Tolnay(serde、anyhow 的作者)開發的 procedural macro,它的做法是把所有 async fn 的回傳值統一包進 Box:
use async_trait::async_trait;
#[async_trait]
trait MyTrait {
async fn do_something(&self) -> String;
}
#[async_trait]
impl MyTrait for MyStruct {
async fn do_something(&self) -> String {
"hello".to_string()
}
}macro 展開後,實際上變成這樣:
trait MyTrait {
fn do_something(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}
impl MyTrait for MyStruct {
fn do_something(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>> {
Box::pin(async move {
"hello".to_string()
})
}
}回傳型別統一成 Pin<Box<dyn Future>>,大小固定,vtable 可以表達,dyn MyTrait 就可以正常運作了。
代價與限制
堆積分配(Heap allocation)
每次呼叫 async trait method,都會觸發一次 Box::pin(),也就是一次 heap 分配。對於高頻呼叫的場景,這個開銷是需要考慮的。
Send bound
預設情況下,async_trait 要求產生的 Future 必須是 Send(可以跨執行緒傳遞),適合多執行緒 async runtime(如 Tokio)。
如果你的情境不需要 Send(例如單執行緒 runtime),可以這樣關掉:
#[async_trait(?Send)]
trait MyTrait {
async fn do_something(&self) -> String;
}生命週期複雜度
macro 會自動處理大部分生命週期,但在一些邊緣情況(例如 &self 裡有複雜的借用關係)還是可能需要手動標注,錯誤訊息也可能比較難讀。
Rust 1.75 的原生支援
Rust 1.75(2023 年 12 月)穩定了 Return Position Impl Trait in Trait(RPITIT),讓 async fn 可以直接用在 trait 裡:
trait MyTrait {
async fn do_something(&self) -> String; // ✅ Rust 1.75+ 可以!
}不需要任何外部 crate,不需要 Box,沒有 heap 分配。
但 dyn Trait 仍有限制
原生支援的版本有一個重要限制:動態派發(dyn MyTrait)尚未完全支援。
fn call(obj: &dyn MyTrait) { // ⚠️ 可能有限制
obj.do_something();
}如果你需要 dyn Trait,目前仍建議使用 async_trait crate,或者搭配 dynosaur 等新興 crate 來橋接。
async_trait 與 Pin 的關係
async_trait 展開後的回傳型別是 Pin<Box<dyn Future>>,這裡的 Pin 是必要的——因為 dyn Future 可能是自引用的(async block 裡可能跨 await 持有引用),必須保證它被 poll 的過程中不會被移動。
這也是為什麼 async_trait 的文件裡,你會看到它與 Pin、Box 密不可分。如果你對 Pin 還不熟悉,可以先閱讀《深入理解 Rust 的 Pin》。
應該用哪個?
| 情境 | 建議 |
|---|---|
Rust 1.75+,不需要 dyn Trait |
原生 async fn in trait |
需要 dyn Trait |
async_trait crate |
| 效能極度敏感,避免 heap 分配 | 考慮手動實作或 impl Trait 參數 |
| 舊版 Rust(< 1.75) | async_trait crate |
總結
- Rust trait 原本不支援
async fn,根本原因是每個實作的 Future 型別不同,無法做到 object-safe。 async_traitcrate 透過把回傳值包進Pin<Box<dyn Future>>解決這個問題,代價是每次呼叫需要 heap 分配。- Rust 1.75 穩定了原生的
async fn in trait,但動態派發(dyn Trait)的支援仍不完整,async_trait在這個場景仍有其價值。