延伸自 理解 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_opPin<&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_project crate 的 #[pin] 標記讓 macro 幫你安全地做 projection
  • tokio::select!&mut pinned_future 語法本質上也是 as_mut() 的 reborrow observability