延伸自 理解 Rust 的 Pin:從問題到解法。
as_mut()是在 Pin 世界裡做 reborrow 的核心操作,如果要手動實作 Future 或 Combinator,你幾乎一定會用到它。
問題:Pin 被消費了怎麼辦?
Future::poll 的簽名長這樣:
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Output>注意 self: Pin<&mut Self> 是值傳遞——Pin<&mut T> 沒有 Copy,傳進函數就被 move 走了。
如果你的 Future 裡面有一個 inner future 需要被 poll,問題就來了:
struct MyFuture {
inner: SomeFuture,
}
impl Future for MyFuture {
type Output = i32;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<i32> {
// inner 也需要 Pin<&mut SomeFuture> 才能被 poll
// 但 self 是 Pin<&mut MyFuture>,
// 不能直接取出 &mut self.inner,因為那樣可能讓 inner 被 move
self.inner.poll(cx) // ❌ SomeFuture::poll 需要 Pin<&mut Self>,不是 &mut Self
}
}as_mut():Pin 的 Reborrow
先看普通 &mut T:Rust 在函數呼叫點自動 reborrow,所以 r 不會被消費:
fn takes_mut(r: &mut i32) { *r += 1; }
let mut x = 0;
let r = &mut x;
takes_mut(r); // Rust 自動 reborrow,等同於 takes_mut(&mut *r)
takes_mut(r); // ✅ r 沒有被消費,可以繼續使用
但 Pin<&mut T> 沒有這個自動 reborrow。把 Pin<&mut T> 傳進函數,它真的會被 move 掉:
fn poll_it<F: Future>(p: Pin<&mut F>, cx: &mut Context<'_>) -> Poll<F::Output> {
p.poll(cx)
}
let mut fut = some_async_fn();
// 用 unsafe 建立 Pin<&mut _> 做示範
let mut pinned = unsafe { Pin::new_unchecked(&mut fut) };
poll_it(pinned, cx); // pinned 被 move 進去,消費掉了
poll_it(pinned, cx); // ❌ 編譯錯誤:use of moved value: `pinned`
Pin::as_mut() 就是為 Pin<&mut T> 補上這個能力——手動 reborrow:
impl<P: DerefMut> Pin<P> {
pub fn as_mut(&mut self) -> Pin<&mut P::Target>
}fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<i32> {
// as_mut() 借出一個新的 Pin<&mut MyFuture>
// 原本的 self 還活著,可以繼續使用
let _pin1 = self.as_mut(); // Pin<&mut MyFuture>
let _pin2 = self.as_mut(); // 還可以再借
}但這樣還是拿到 Pin<&mut MyFuture>,不是 Pin<&mut SomeFuture>,怎麼取得 inner 欄位的 Pin?
Projection:從外層 Pin 投影到內層欄位
「把 Pin<&mut Outer> 轉成 Pin<&mut Inner>」這個操作叫做 Pin Projection(投影)。
手動做需要 unsafe:
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<i32> {
// as_mut() 先做 reborrow
// map_unchecked_mut() 再投影到 inner 欄位
let inner_pin: Pin<&mut SomeFuture> = unsafe {
self.as_mut().map_unchecked_mut(|s| &mut s.inner)
};
inner_pin.poll(cx)
}map_unchecked_mut 標記為 unsafe 是因為你必須自己保證:
- inner 欄位在整個
MyFuture被 drop 之前不會被 move 出去 - 你不會同時製造兩個指向同一個欄位的可變引用
實務:用 pin_project 消除 unsafe
手動寫 projection 容易出錯,實務上幾乎都用 pin-project crate:
use pin_project::pin_project;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
#[pin_project]
struct Timeout<F: Future> {
#[pin] // ← 標記這個欄位需要被 pin
inner: F,
deadline: std::time::Instant,
}
impl<F: Future> Future for Timeout<F> {
type Output = Option<F::Output>;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let this = self.project(); // 安全地做 projection,無需 unsafe
// this.inner 的型別是 Pin<&mut F> ← 已經 pin 好,可以直接 poll
// this.deadline 的型別是 &mut Instant ← 普通可變引用
if std::time::Instant::now() >= *this.deadline {
return Poll::Ready(None);
}
match this.inner.poll(cx) {
Poll::Ready(v) => Poll::Ready(Some(v)),
Poll::Pending => Poll::Pending,
}
}
}#[pin_project] 這個 proc macro 幫你在背後生成了安全的 projection 程式碼,你只需要在欄位上標 #[pin] 就好。
as_mut() 在 select! 裡的作用
as_mut() 不只在自訂 Future 裡用到,tokio::select! 在 loop 裡重複 poll 同一個 Future 時,也依賴相同的概念:
use tokio::pin;
let long_op = some_slow_task();
pin!(long_op); // 現在 long_op 是 Pin<&mut impl Future>
loop {
tokio::select! {
result = &mut long_op => { // &mut 在這裡做的正是 reborrow
println!("完成:{:?}", result);
break;
}
_ = tokio::time::sleep(Duration::from_secs(1)) => {
println!("還在等...");
}
}
}&mut long_op 對 Pin<&mut F> 做 reborrow,每次進入 select! 都借出一個新的 Pin<&mut F>,讓同一個 future 可以被跨多次迴圈反覆 poll,狀態不會丟失。
整理:哪時用哪個方法?
Pin<&mut T>
├── as_mut() → Pin<&mut T>
│ ├── 用於:reborrow,讓同一個 Pin 可以被多次使用
│ └── 然後可以接 map_unchecked_mut() 做 projection(通常讓 pin_project 來)
│
├── get_mut() → &mut T (只有 T: Unpin 才能用)
│ └── 用於:徹底解除 Pin 約束,拿回普通可變引用
│
└── map_unchecked_mut() → Pin<&mut U> (unsafe)
└── 用於:手動 projection 到欄位
小結
as_mut()是 Pin 版的 reborrow,解決「Pin<&mut T>傳進函數就消費掉」的問題- 手動 Future 實作幾乎都需要
as_mut()+ projection 來取得 inner future 的 Pin - 實務上用
pin_projectcrate 的#[pin]標記讓 macro 幫你安全地做 projection tokio::select!的&mut pinned_future語法本質上也是as_mut()的 reborrow observability