本文整理一种对 Move trait 的质疑:不可移动性更适合描述为值在特定内存位置上的状态。若将其作为类型分类,可能增加泛型约束和原地初始化的复杂度。
来源
- 知乎回答:如何看待 rust 2026 roadmap 计划实现 Move Trait, 移除 Pin?
- 作者:酱紫君(@GalAster)
- 原文归档:
raw/zhihu/rust-2026-roadmap-move-trait-remove-pin.md - 相关提案:https://github.com/rust-lang/rfcs/issues/3709
- 参考:https://without.boats/blog/pinned-places/
核心问题
Rust 的 Pin<P> 机制是为了解决自引用结构、异步 Future 等对象在被观察到真实地址后不能再 relocation 的问题。争议在于:
- 当前机制把“固定”放在指针包装类型
Pin<P>上; - Roadmap 中讨论的
Movetrait /!Move方向倾向于把“能否移动”提升为类型分类; - 原文认为,更准确的语义应是 某个具体 Place 在运行流程中进入 pinned 状态,而不是类型天生属于
Move或!Move家族。
Pin 的现状:库层机制
Pin 的核心思想是:在不改变 Rust 底层 move 语义的前提下,用包装类型限制访问通路。
| 维度 | 当前 Pin<P> 方案 |
|---|---|
| 位置 | 标准库/类型层包装 |
| 语义 | Pin<P> 不给 !Unpin 目标暴露普通 &mut T |
| 好处 | 编译器底层 move 语义无需大改,兼容性好 |
| 坏处 | 形成 &mut T 与 Pin<&mut T> 两套平行引用体系;field projection 依赖 pin_project! 等宏 |
简化理解:类型 T 自身不变,变化的是“你通过什么路径访问它”。当一个指针被包装进 Pin,且目标没有实现 Unpin 时,安全 API 不再允许拿到可移动的 &mut T。
Move trait 方向的问题
如果引入默认 auto trait Move,并把不可移动性建模成 !Move,就等于把 Rust 类型划分为两个互斥集合:
原文主要提出了三个问题。
1. 不可移动性不是类型的先天属性
自引用 Future 刚被创建时通常仍是普通值,可以被移动到堆上、放进容器或传给执行器;只有在被 poll()、地址被见证(witness)之后,它才进入不可移动状态。
也就是说,一个对象常经历如下状态转移:
其中:
| 状态 | 作用对象 | 含义 |
|---|---|---|
T.own(b) | 值 / 字节序列 | 尚未绑定物理地址,可 relocation |
T.shr(a, p) | 生命周期内共享指针 | 在生命周期 a 内保持共享不变性 |
T.pin(p) | 物理指针 / Place | 指针 p 指向的位置托管一个已固定的实例 |
关键点:own(b) 作用于值,pin(p) 作用于物理位置。不可移动性更像特定 Place 上的 typestate,而不是 T 本身的永久分类。
2. 泛型污染可能比 ?Sized 更严重
Rust 曾默认给泛型参数加 T: Sized,后来为了动态大小类型引入 ?Sized。如果 Move 也成为默认约束,生态中大量 API 可能被迫写成:
pub struct Vec<T: ?Move> { ... }
pub trait Iterator { type Item: ?Move; fn next(&mut self) -> Option<Self::Item>;}
pub trait Future { type Output: ?Move; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;}但 Move 比 Sized 更底层:函数调用、返回值、闭包捕获、容器存储、栈帧搬移都涉及 move 语义。!Move 作为值如何传参、压栈、弹栈、返回,会迫使语言引入复杂的隐式引用/指针传递机制。
3. !Move 需要配套的原地初始化机制
在现行 Pin 体系下,异步任务可以先作为普通值构造、移动、装箱,最后在执行器里固定:
async fn my_task() { ... }
fn spawn_task() { let fut = my_task(); // 普通值 let boxed_fut = Box::new(fut); // 可移动到堆上 let mut pinned_fut = Box::into_pin(boxed_fut); // 固定后 poll pinned_fut.as_mut().poll(...);}若 my_task() 直接返回 !Move 类型,那么 Box::new(fut)、函数返回、容器存放都可能被禁止。为了解决构造问题,语言必须支持更复杂的原地初始化(in-place initialization)与 panic 时部分初始化字段的 drop glue,这会显著增加编译器与用户心智负担。
替代方向:Typestate on Places
原文更认可 Pinned places 思路:把不可移动性作为借用检查器追踪的 Place 状态。
在 MIR 层面,一个 Place 是局部变量加投影(如 x.y、*x)。可以为每个 Place 维护:
规则大致为:
| 规则 | 含义 |
|---|---|
let mut x: T | 初始为 Unpinned |
&pin mut x 或 let pin mut x | 触发 witness,状态单向转为 Pinned |
State(x) == Pinned | 禁止移动 x,禁止普通 &mut x |
&pin mut x | 允许重复获取固定借用 |
Drop(x) | 必须 in-place 运行析构 |
前端语法可以想象为:
let mut my_task = async_task();let my_task = pass_around(my_task);let pin mut my_task = my_task;let error = my_task; // 编译期报错:Pinned place 不能再移动同时引入原生 &pin mut T:
fn poll_future<T>(fut: &pin mut T) { ... }Field Projection 的意义
如果 &pin mut T 成为原生引用,编译器可以内建 field projection:
struct MyStruct { unpin_field: i32, pinned_field: MySelfReferential,}
fn process(s: &pin mut MyStruct) { let f1: &mut i32 = &mut s.unpin_field; let f2: &pin mut MySelfReferential = &pin mut s.pinned_field;}解释:
i32: Unpin,即使外部结构体被固定,这个字段仍可安全降级为普通&mut i32;MySelfReferential: !Unpin,字段地址与外部结构体绑定,必须投影为&pin mut MySelfReferential;- 这样可以减少
pin-project宏和手写 unsafe 投影。
设计取舍
原文主张由编译器追踪 Place 的固定状态,以描述自引用对象从可移动到不可移动的过程。相比新增 Move 类型分类,这可能减少泛型和调用约定的改动;具体取舍仍取决于提案设计。
与相关概念的关系
| 概念 | 与本文关系 |
|---|---|
Pin<P> | 当前库层机制,用包装指针限制可变访问 |
Unpin | 标记类型可从 pinned 状态平凡退回 movable/owned 语义 |
Move trait | Roadmap 讨论方向,可能把 moveability 类型化 |
| Typestate | 本文主张:pinning 更像 Place 的状态转移 |
| Pinned places | 更接近本文推荐方向:由借用检查器追踪 pinned Place |
&pin mut T | 可能替代 Pin<&mut T> 的原生固定可变引用 |
后续问题
- RFC issue #3709 的后续讨论是否仍沿着
&pin mut T/ pinned places 方向推进。 - Rust roadmap 中
Movetrait 的真实设计是否确实会引入?Move风格泛型污染,还是有其他缓解方案。 - Ralf Jung 的 pinning formal model 与 RustBelt/Iris 体系如何精确定义
T.own、T.shr、T.pin。