大家好,今天我们来深入探讨 Rust 中另一个非常重要但又有些微妙的概念:Unpin Trait。要理解 Unpin,我们首先需要了解它的“另一半”——Pin(钉住/固定)。
1.前置概念 - 为什么要“钉住” (Pin)?
昨天已经写了一篇 Rust的Pin并不可怕,今天就来学一学
在 Rust 中,值默认是可以被移动(move)的。当我们把一个值从一个变量赋给另一个变量,或者把它传入一个函数时,它的内存地址可能会改变。对于大多数类型(如 i32, String, Vec)来说,这完全没问题。
但是,存在一类特殊的结构,它们被称为自引用结构 (Self-Referential Structs) 。这种结构内部包含一个指向其自身字段的指针。
思考一下: 如果我们移动了一个自引用结构,会发生什么?
答案是:结构体被移动到新的内存地址,但它内部的指针还指向旧的、无效的地址。当我们试图使用这个指针时,就会导致未定义行为(Undefined Behavior),通常是程序崩溃。
为了解决这个问题,Rust 引入了 Pin。Pin<T> 是一种智能指针,它向编译器做出一个承诺:被 Pin 包裹起来的值,其内存地址将不会被改变。这使得我们可以安全地创建和使用自引用结构,这在异步编程的 Future 中尤为关键。
2.Unpin Trait 是什么?
现在我们知道了 Pin 是用来“钉住”那些不能被移动的值的。那么 Unpin 是什么呢?
Unpin 是一个标记(Marker Trait),它告诉编译器: “这个类型就算被 Pin 包裹了,移动它也是完全安全的。”
换句话说,如果一个类型实现了 Unpin,它就表示自己“不介意被固定”,或者说它没有自引用这类必须保持地址不变的内部结构。当 Pin 遇到一个 Unpin 的类型时,Pin 的严格限制就会被“放开”,允许我们像操作普通类型一样操作它。
核心:
Unpin是一个 auto trait。这意味着编译器会为你自动实现它。- 一个自定义类型默认是
Unpin的,前提是它的所有字段也都是Unpin的。 - Rust 中绝大多数标准库类型,如
String,Vec<T>,i32,bool等,都实现了Unpin。
3.Unpin 的实际影响 - 代码示例
Unpin 最直接的影响体现在当你试图从 Pin<&mut T> 获取一个 &mut T 时。
- 如果
T: Unpin,你可以安全地从Pin<&mut T>得到&mut T。 - 如果
T: !Unpin(即T没有实现Unpin),你不能安全地从Pin<&mut T>得到&mut T。这是Pin的核心安全保障。
让我们来看官方文档中的例子:
use std::mem;
use std::pin::Pin;
fn main() {
// String 类型是 Unpin 的
let mut string = "this".to_string();
let mut pinned_string: Pin<&mutString> = Pin::new(&mut string);
// 因为 String 是 Unpin 的,我们可以从 Pin<&mut String> 获得 &mut String
// Pin::deref_mut() 会隐式调用,允许我们获得可变引用
// 然后我们就可以安全地替换它的内容
mem::replace(&mut *pinned_string, "other".to_string());
println!("String is now: {}", pinned_string);
}
- 我们创建了一个
String,它是一个Unpin类型。 - 我们用
Pin::new将它的可变引用“钉住”。 - 因为
String是Unpin的,Pin允许我们通过DerefMuttrait 拿到&mut String。 mem::replace需要一个&mut T参数,我们成功地提供了它,并替换了字符串的内容。这证明了对于Unpin类型,Pin的限制被放宽了。
4.如何让类型 !Unpin (Not Unpin)?
既然大多数类型默认都是 Unpin,那我们如何创建一个必须被“钉住”的自引用结构呢?我们需要一种方法来告诉编译器:“我的这个类型是不能随便移动的,请不要为我自动实现 Unpin。”
方法就是使用 std::marker::PhantomPinned。
PhantomPinned 是一个零大小的标记类型,它本身没有实现 Unpin。根据 auto trait 的规则,任何包含一个 !Unpin 字段的结构体,其本身也会变成 !Unpin。
示例:创建一个自引用(因此 !Unpin)的结构体
use std::marker::PhantomPinned;
use std::pin::Pin;
use std::ptr::NonNull;
// 这是一个自引用结构体。它包含数据和一个指向该数据的指针。
// 为了保证安全,它必须是 !Unpin。
struct Unmovable {
data: String,
// slice 是一个指向 data 第一个字节的裸指针
slice: NonNull<u8>,
// 这个字段让整个结构体变为 !Unpin
_pin: PhantomPinned,
}
impl Unmovable {
// 我们不再使用一个简单的 `new` 函数直接返回 Self,因为它无法安全地创建自引用。
// 相反,我们先创建一个 "未初始化" 的实例。
fn new(data: String) -> Self {
Unmovable {
data,
// 先用一个临时的、无效的指针。
// `NonNull::dangling()` 提供一个非空但无效的指针,用于初始化。
slice: NonNull::dangling(),
_pin: PhantomPinned,
}
}
// 然后,我们提供一个 `init` 方法来建立自引用。
// 这个方法要求实例已经被 Pin 住,从而保证它不会再被移动。
fn init(self: Pin<&mut Self>) {
// `self.data` 的地址现在是固定的了。
// 我们可以安全地获取指向它的指针。
let slice_ptr = self.data.as_ptr();
// `unsafe` 块是必需的,因为我们要绕过 Pin 的保护来修改字段。
// 我们知道这是安全的,因为我们只是在初始化一个指针,
// 并且 Pin 保证了 `self` 的地址是稳定的。
let this = unsafe { self.get_unchecked_mut() };
this.slice = unsafe { NonNull::new_unchecked(slice_ptr as *mut u8) };
}
// get_slice 方法现在也需要一个 Pin 住的引用,确保安全。
fn get_slice(self: Pin<&Self>) -> &str {
// 因为我们知道 self 被 Pin 住,所以可以安全地解引用内部指针
let slice_bytes = unsafe {
std::slice::from_raw_parts(self.slice.as_ptr(), self.data.len())
};
std::str::from_utf8(slice_bytes).unwrap()
}
}
fn main() {
// 1. 创建一个实例。此时它的 `slice` 指针是无效的。
let data = Unmovable::new("hello".to_string());
// 2. 将它放到堆上并 Pin 住。`Box::pin` 可以做到这一点。
// 从这里开始,`pinned_data` 的内存地址就不会再改变了。
let mut pinned_data = Box::pin(data);
// 3. 调用 `init` 来安全地建立自引用。
// `as_mut()` 可以从 `Pin<Box<T>>` 获得 `Pin<&mut T>`。
pinned_data.as_mut().init();
// 4. 现在可以安全地使用它了。
// `as_ref()` 可以从 `Pin<Box<T>>` 获得 `Pin<&T>`。
let slice = pinned_data.as_ref().get_slice();
println!("Slice: {:?}", slice); // 输出: Slice: "hello"
// 下面的代码仍然无法通过编译,这证明了 Pin 的作用:
// `mem::replace` 需要 `&mut Unmovable`,但我们无法从 `Pin<Box<Unmovable>>`
// (当 Unmovable 是 !Unpin 时) 安全地获得它。
// std::mem::replace(&mut *pinned_data, Unmovable::new("world".to_string()));
}
1.Unmovable::new 现在只创建一个“部分完成”的实例,其内部指针 slice 是一个临时的无效值 (NonNull::dangling)。
2.在 main 函数中,我们立即使用 Box::pin 将这个实例固定在堆上。这是关键步骤,此后 pinned_data 的内存地址不会再改变。
3.我们定义了一个新的方法 init,它的 self 参数是 Pin<&mut Self>。这保证了只有被固定的实例才能调用它。
4.在 init 内部,因为我们知道实例地址是稳定的,所以可以安全地获取 data 的地址并赋值给 slice 字段。这里需要 unsafe 代码,因为我们在 Pin 的保护下修改数据,但我们很清楚这个操作的上下文是安全的。
5.完成初始化后,get_slice 方法也要求 Pin<&Self>,确保在读取数据时,实例也不会被移动。
5.Unpin 在异步编程中的角色
Unpin 在 async/await 中扮演着至关重要的角色。一个 async 函数会生成一个 Future。
- 如果
Future是自引用的(例如,它在await点之间保存了对局部变量的引用),那么这个Future就是!Unpin的。在调用它的.poll()方法之前,必须先将它Pin住。 - 如果
Future不是自引用的,那么它就是Unpin的。这样的Future可以被更自由地处理,无需强制固定。
Unpin trait 使得 Future 的 API 设计可以兼顾两全:既能通过 Pin 保证自引用 Future 的内存安全,又能让非自引用的 Future 免于 Pin 的使用复杂性,更加符合人体工程学。
最后总结一下
| 特性 | T: Unpin (例如 String) | T: !Unpin (例如 Unmovable) |
|---|---|---|
| 含义 | 移动此类型是安全的,即使它被 Pin 包裹。 | 移动此类型是危险的,Pin 必须保证其地址不变。 |
| 来源 | 默认。只要所有字段都是 Unpin,类型就是 Unpin。 | 通过添加 PhantomPinned 字段来显式选择退出 Unpin。 |
| Pin 的行为 | Pin<&mut T> 几乎等同于 &mut T,限制被放宽。 | Pin<&mut T> 严格受限,不能安全地获得 &mut T。 |
| 使用场景 | 大多数普通数据类型。 | 自引用结构,异步 Future。 |
希望这份教程能帮助大家彻底理解 Unpin 的概念!这是一个深入 Rust 内存安全模型和异步编程核心的绝佳切入点。
看完了,欢迎大家关注我们公众号 猩猩程序员