目录
- Rust 智能指针详解
- 一、普通指针、引用和智能指针的区别
- 二、智能指针的两个核心 Trait
- 三、Box<T>:堆上单一所有权
- 四、Rc<T>:单线程共享所有权
- 五、RefCell<T>:运行时借用检查
- 六、Rc<RefCell<T>>:单线程共享可变数据
- 七、Weak<T>:弱引用与循环引用
- 八、Arc<T>:多线程共享所有权
- 九、Arc<Mutex<T>>:多线程共享可变数据
- 十、Arc<RwLock<T>>:多读单写
- 十一、Cell<T>:复制型内部可变性
- 十二、Cow<T>:写时复制
- 十三、Pin<P>:固定对象的内存位置
- 十四、Box<T>、Rc<T>、Arc<T> 的核心区别
- 十五、常见组合对照
- 十六、一个综合案例:共享应用状态
- 十七、常见误区
- 十八、选择智能指针的判断流程
- Box::pin 是什么?
- 一、Box::new 和 Box::pin 的区别
- 二、什么叫“值被移动”?
- 三、为什么已经放到堆上,还需要 Pin?
- 四、Box::pin 到底固定了谁?
- 五、Box::pin 的基本使用
- 六、为什么普通 String 使用 Box::pin 好像没区别?
- 七、Unpin 是什么?
- 八、如何创建一个 !Unpin 类型?
- 九、Pin<Box<T>> 能不能修改内部值?
- 十、Box::pin 与 Box::new 加 Box::into_pin
- 十一、最常见的使用场景:异步 Future
- 十二、为什么返回异步 Trait 对象时经常看到 Pin<Box<dyn Future>>?
- 十三、在实际业务开发中什么时候会遇到?
- 十四、一个地址演示
- 十五、Box::pin 不等于绝对不可移动
- 十六、什么时候应该使用 Box::pin?
- 十七、一句话记忆
Rust 智能指针详解
Rust 中的“智能指针”不是一种特殊语法,而是一类像指针一样使用、但额外携带管理能力的结构体。
普通引用 &T 只负责借用数据;智能指针通常还可以负责:
- 拥有数据;
- 在堆上分配数据;
- 统计引用数量;
- 控制共享与可变访问;
- 在离开作用域时自动释放资源;
- 在线程之间安全共享数据。
Rust 官方重点介绍的智能指针包括 Box<T>、Rc<T>、RefCell<T>,并通过 Deref 和 Drop 实现类似普通引用的访问及自动清理。(doc.rust-lang.org)
一、普通指针、引用和智能指针的区别
1. 普通引用
fn main() {
let value = 100;
let reference = &value;
println!("{}", *reference);
}
这里:
value ↑ &value
reference:
- 不拥有
value; - 只是临时借用;
- 不能比
value活得更久; - 不负责释放
value。
引用本质上类似一个可以访问其他变量数据的地址,但数据仍由原变量拥有。(doc.rust-lang.org)
2. 智能指针
fn main() {
let value = Box::new(100);
println!("{}", *value);
}
这里的 value 是:
Box<i32>
内存结构可以简单理解为:
栈 Stack 堆 Heap ┌───────────┐ ┌───────┐ │ Box 指针 │ ────────→ │ 100 │ └───────────┘ └───────┘
Box<i32>:
- 拥有堆上的
100; - 可以通过
*value解引用; - 当
Box离开作用域时,堆内存会被自动释放。
二、智能指针的两个核心 Trait
理解 Rust 智能指针,必须理解:
Deref Drop
1. Deref:让类型表现得像引用
Deref 决定:
*智能指针
得到什么。
下面实现一个简化版智能指针。
use std::ops::Deref;
struct MyBox<T> {
value: T,
}
impl<T> MyBox<T> {
fn new(value: T) -> Self {
Self { value }
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
fn main() {
let number = MyBox::new(100);
assert_eq!(100, *number);
}
这句:
*number
概念上相当于:
*(number.deref())
但一般不直接调用:
number.deref()
而是让 Rust 自动处理。
2. Deref 自动转换
假设有一个函数:
fn print_name(name: &str) {
println!("姓名:{name}");
}
调用时可以传入:
fn main() {
let name = String::from("Albert");
print_name(&name);
}
虽然:
&name
类型是:
&String
函数需要:
&str
但因为 String 实现了:
Deref<Target = str>
Rust 自动进行转换:
&String ↓ Deref &str
这叫:
Deref coercion 解引用强制转换
它也是为什么以下代码可以工作:
let boxed = Box::new(String::from("hello"));
println!("{}", boxed.len());
实际调用链可以理解为:
Box<String>
↓ Deref
String
↓ Deref
str
3. Drop:离开作用域时自动清理
struct DatabaseConnection {
name: String,
}
impl Drop for DatabaseConnection {
fn drop(&mut self) {
println!("关闭数据库连接:{}", self.name);
}
}
fn main() {
{
let connection = DatabaseConnection {
name: String::from("MySQL"),
};
println!("正在使用数据库:{}", connection.name);
}
println!("程序继续执行");
}
输出:
正在使用数据库:MySQL 关闭数据库连接:MySQL 程序继续执行
Drop 常用于自动释放:
- 堆内存;
- 文件句柄;
- 网络连接;
- 数据库连接;
- 锁;
- 操作系统资源。
Rust 官方文档说明,Drop 会在值即将离开作用域时执行自定义清理代码。(doc.rust-lang.org)
不能直接这样调用:
connection.drop();
需要提前释放时,应使用:
drop(connection);
例如:
use std::mem::drop;
fn main() {
let value = String::from("hello");
drop(value);
// println!("{value}"); // 编译错误,value 已被移动并释放
}
三、Box<T>:堆上单一所有权
Box<T> 是最简单、最常用的智能指针。
它表示:
数据存放在堆上,但只有一个所有者。
官方文档将 Box<T> 描述为在堆上存储数据的智能指针,栈中保留指向堆数据的指针。(doc.rust-lang.org)
1. 基本使用
fn main() {
let number = Box::new(100);
println!("number = {}", number);
println!("number = {}", *number);
}
Box<T> 实现了 Deref,所以很多时候不需要显式写 *。
2. Box<T> 的所有权
fn main() {
let box1 = Box::new(String::from("hello"));
let box2 = box1;
// println!("{box1}"); // 编译错误
println!("{box2}");
}
这和普通 String 一样发生所有权移动:
box1 ──move──> box2
堆上的字符串没有复制,只是所有权从 box1 转移给 box2。
需要复制里面的数据时:
fn main() {
let box1 = Box::new(String::from("hello"));
let box2 = box1.clone();
println!("{box1}");
println!("{box2}");
}
这里是深度克隆,创建两份不同的 String 数据。
3. 为什么需要 Box<T>
场景一:递归类型
下面的类型无法编译:
enum List {
Node(i32, List),
Empty,
}
问题是编译器无法计算 List 的大小:
List
└─ List
└─ List
└─ List
...
使用 Box<List> 后可以解决:
enum List {
Node(i32, Box<List>),
Empty,
}
use List::{Empty, Node};
fn main() {
let list = Node(
1,
Box::new(Node(
2,
Box::new(Node(
3,
Box::new(Empty),
)),
)),
);
print_list(&list);
}
fn print_list(list: &List) {
match list {
List::Node(value, next) => {
println!("{value}");
print_list(next);
}
List::Empty => {}
}
}
因为 Box<List> 本身大小固定,就是一个指针大小,所以整个枚举大小可以确定。
场景二:Trait 对象
当运行时才确定具体实现类型时,可以使用:
Box<dyn Trait>
例如:
trait Notification {
fn send(&self);
}
struct EmailNotification;
impl Notification for EmailNotification {
fn send(&self) {
println!("发送邮件通知");
}
}
struct SmsNotification;
impl Notification for SmsNotification {
fn send(&self) {
println!("发送短信通知");
}
}
fn create_notification(use_email: bool) -> Box<dyn Notification> {
if use_email {
Box::new(EmailNotification)
} else {
Box::new(SmsNotification)
}
}
fn main() {
let notification = create_notification(true);
notification.send();
}
因为 EmailNotification 和 SmsNotification 是不同类型,函数不能直接根据条件返回两种具体类型。
但是它们都实现了:
Notification
所以可以统一返回:
Box<dyn Notification>
场景三:把较大的对象放到堆上
struct LargeData {
buffer: [u8; 1024 * 1024],
}
fn main() {
let data = Box::new(LargeData {
buffer: [0; 1024 * 1024],
});
println!("{}", data.buffer.len());
}
不过,不应该看到大对象就机械地使用 Box。应根据:
- 对象生命周期;
- 是否需要递归;
- 是否需要 Trait 对象;
- 是否需要稳定地址;
- 栈空间压力;
综合判断。
四、Rc<T>:单线程共享所有权
Rc 的全称是:
Reference Counted 引用计数
它解决的问题是:
一个值需要被多个变量共同拥有。
Rc<T> 会维护强引用计数;当最后一个强引用被销毁时,内部数据才会被释放。(doc.rust-lang.org)
1. 为什么普通所有权不够
fn main() {
let data = String::from("共享数据");
let owner1 = data;
// let owner2 = data; // 编译错误
}
一个普通值只能有一个所有者。
使用 Rc<T>:
use std::rc::Rc;
fn main() {
let data = Rc::new(String::from("共享数据"));
let owner1 = Rc::clone(&data);
let owner2 = Rc::clone(&data);
println!("data = {data}");
println!("owner1 = {owner1}");
println!("owner2 = {owner2}");
println!("强引用数量:{}", Rc::strong_count(&data));
}
输出类似:
data = 共享数据 owner1 = 共享数据 owner2 = 共享数据 强引用数量:3
内存结构:
data ─────┐
owner1 ───┼──→ Rc 控制块 ──→ String
owner2 ───┘ │
└─ strong_count = 3
2. Rc::clone 不会深度复制数据
let owner2 = Rc::clone(&owner1);
它通常只会:
- 增加强引用计数;
- 复制指向同一块分配的指针。
不会复制内部的整个对象。
推荐写法:
Rc::clone(&data)
虽然下面也能工作:
data.clone()
但 Rc::clone(&data) 更清楚地表达:
这里只增加引用计数,不是在深度复制业务对象。
3. 引用计数变化
use std::rc::Rc;
fn main() {
let data = Rc::new(String::from("hello"));
println!("{}", Rc::strong_count(&data)); // 1
{
let data2 = Rc::clone(&data);
println!("{}", Rc::strong_count(&data)); // 2
{
let data3 = Rc::clone(&data);
println!("{}", Rc::strong_count(&data)); // 3
println!("{data3}");
}
println!("{}", Rc::strong_count(&data)); // 2
println!("{data2}");
}
println!("{}", Rc::strong_count(&data)); // 1
}
4. Rc<T> 默认不能修改内部数据
下面不能编译:
use std::rc::Rc;
fn main() {
let data = Rc::new(String::from("hello"));
// data.push_str(" world");
}
原因是:
Rc 提供多个所有者。
如果所有所有者都能随意取得可变引用,就可能同时修改同一份数据,违反 Rust 的借用规则。
所以:
Rc<T> = 单线程 + 多所有权 + 默认只读
5. Rc<T> 不能跨线程
下面不能编译:
use std::rc::Rc;
use std::thread;
fn main() {
let value = Rc::new(100);
thread::spawn(move || {
println!("{value}");
});
}
因为 Rc<T> 的引用计数更新不是线程安全的,不实现线程传递所需的 Send 和 Sync。跨线程共享应该使用 Arc<T>。(doc.rust-lang.org)
五、RefCell<T>:运行时借用检查
普通 Rust 借用规则由编译器检查:
任意数量的不可变引用 或者 一个可变引用
RefCell<T> 并没有取消规则,只是把检查时间从:
编译期
推迟到了:
运行时
RefCell<T> 支持“内部可变性”:即使外层变量没有声明为 mut,也可以通过运行时借用检查修改内部数据。(doc.rust-lang.org)
1. 基本使用
use std::cell::RefCell;
fn main() {
let message = RefCell::new(String::from("hello"));
message.borrow_mut().push_str(" world");
println!("{}", message.borrow());
}
注意:
let message
没有声明:
let mut message
但内部字符串仍然被修改了。
这是因为修改的是:
RefCell 内部的数据
而不是重新给 message 赋值。
2. borrow() 和 borrow_mut()
borrow()
返回:
Ref<T>
相当于运行时不可变借用。
borrow_mut()
返回:
RefMut<T>
相当于运行时可变借用。
例如:
use std::cell::RefCell;
fn main() {
let number = RefCell::new(10);
{
let value = number.borrow();
println!("{value}");
}
{
let mut value = number.borrow_mut();
*value += 5;
}
println!("{}", number.borrow());
}
3. 借用冲突会发生运行时 panic
下面代码可以编译,但运行会崩溃:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(String::from("hello"));
let mut borrow1 = data.borrow_mut();
// 第二次可变借用,运行时 panic
let mut borrow2 = data.borrow_mut();
borrow1.push('!');
borrow2.push('?');
}
典型错误:
already borrowed: BorrowMutError
普通引用的错误发生在编译期:
let mut text = String::from("hello");
let reference1 = &mut text;
let reference2 = &mut text;
RefCell 的错误发生在运行期。
因此:
RefCell 不是放宽借用规则, 而是把借用规则推迟到运行时检查。
4. 控制借用作用域
错误示例:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
let read = data.borrow();
// read 还活着,不能取得可变借用
// data.borrow_mut().push(4);
println!("{read:?}");
}
正确方式:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
{
let read = data.borrow();
println!("{read:?}");
}
data.borrow_mut().push(4);
println!("{:?}", data.borrow());
}
也可以显式释放:
let read = data.borrow();
println!("{read:?}");
drop(read);
data.borrow_mut().push(4);
六、Rc<RefCell<T>>:单线程共享可变数据
这是 Rust 中非常常见的组合:
Rc<RefCell<T>>
分别解决:
Rc → 多个所有者 RefCell → 内部可变性
组合起来:
单线程环境中,多个所有者共享并修改同一份数据。
1. 基本示例
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct User {
name: String,
login_count: u32,
}
fn main() {
let user = Rc::new(RefCell::new(User {
name: String::from("Albert"),
login_count: 0,
}));
let module_a = Rc::clone(&user);
let module_b = Rc::clone(&user);
module_a.borrow_mut().login_count += 1;
module_b.borrow_mut().login_count += 1;
println!("{:#?}", user.borrow());
}
最终:
login_count = 2
访问过程:
Rc<RefCell<User>>
│
├─ Rc::clone:共享所有权
│
└─ borrow_mut:修改内部 User
2. 为什么不是 RefCell<Rc<T>>
这两种含义不同。
Rc<RefCell<T>>
Rc<RefCell<User>>
表示:
多人共享同一个可变 User。
RefCell<Rc<T>>
RefCell<Rc<User>>
表示:
外层 RefCell 内保存一个 Rc, 可以把这个 Rc 替换成指向另一个 User 的 Rc, 但不代表能修改 User 本身。
例如:
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let user1 = Rc::new(String::from("Alice"));
let user2 = Rc::new(String::from("Bob"));
let current_user = RefCell::new(Rc::clone(&user1));
*current_user.borrow_mut() = Rc::clone(&user2);
println!("{}", current_user.borrow());
}
这里修改的是:
RefCell 里面保存的 Rc 指针
不是修改 String 本身。
七、Weak<T>:弱引用与循环引用
Rc<T> 的强引用会维持数据存活。
Weak<T>:
- 可以指向
Rc管理的数据; - 不增加强引用计数;
- 不拥有内部对象;
- 不保证对象还活着;
- 需要通过
upgrade()尝试升级为Rc<T>。
官方文档指出,Weak 不会阻止内部值被释放,常用于避免 Rc 或 Arc 形成无法释放的循环引用。(doc.rust-lang.org)
1. 基本使用
use std::rc::{Rc, Weak};
fn main() {
let strong = Rc::new(String::from("hello"));
let weak: Weak<String> = Rc::downgrade(&strong);
println!("强引用:{}", Rc::strong_count(&strong));
println!("弱引用:{}", Rc::weak_count(&strong));
match weak.upgrade() {
Some(value) => println!("对象仍存在:{value}"),
None => println!("对象已被释放"),
}
drop(strong);
match weak.upgrade() {
Some(value) => println!("对象仍存在:{value}"),
None => println!("对象已被释放"),
}
}
输出类似:
强引用:1 弱引用:1 对象仍存在:hello 对象已被释放
为什么 upgrade() 返回:
Option<Rc<T>>
因为弱引用指向的对象可能已经释放。
2. 循环引用为什么会内存泄漏
假设两个对象互相通过 Rc 持有:
A ──Rc──→ B ↑ │ └──Rc─────┘
即使外部变量都销毁了:
A 的强引用数量仍不为 0 B 的强引用数量仍不为 0
因此两者都不会释放。
Rust 能保证这里不会出现悬空指针或非法内存访问,但可能发生:
逻辑上的内存泄漏
Rust 官方文档明确说明,引用计数循环可能导致内存无法释放,应使用 Weak 打破所有权循环。(doc.rust-lang.org)
3. 父子节点示例
合理的树结构通常是:
父节点 ──Rc──→ 子节点 父节点 ←Weak── 子节点
父节点拥有子节点,但子节点不应该反过来拥有父节点。
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let parent = Rc::new(Node {
value: 1,
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
});
let child = Rc::new(Node {
value: 2,
parent: RefCell::new(Rc::downgrade(&parent)),
children: RefCell::new(Vec::new()),
});
parent
.children
.borrow_mut()
.push(Rc::clone(&child));
let child_parent = child.parent.borrow().upgrade();
match child_parent {
Some(node) => {
println!("子节点的父节点:{}", node.value);
}
None => {
println!("父节点已被释放");
}
}
}
八、Arc<T>:多线程共享所有权
Arc 的全称是:
Atomic Reference Counted 原子引用计数
它相当于线程安全版本的 Rc<T>。
Rc<T> → 单线程共享所有权 Arc<T> → 多线程共享所有权
1. 多线程只读共享
use std::sync::Arc;
use std::thread;
fn main() {
let config = Arc::new(String::from("production"));
let mut handles = Vec::new();
for id in 1..=3 {
let config = Arc::clone(&config);
let handle = thread::spawn(move || {
println!("线程 {id} 读取配置:{config}");
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
三个线程持有同一个字符串。
2. Arc<T> 并不自动提供可变性
下面不能直接修改:
use std::sync::Arc;
fn main() {
let data = Arc::new(Vec::<i32>::new());
// data.push(1); // 编译错误
}
Arc<T> 只解决:
多个线程共同拥有数据
不解决:
多个线程共同修改数据
多线程共享修改一般使用:
Arc<Mutex<T>>
或者:
Arc<RwLock<T>>
九、Arc<Mutex<T>>:多线程共享可变数据
这是并发开发中非常常见的组合:
Arc<Mutex<T>>
分别解决:
Arc → 多线程共享所有权 Mutex → 同一时间只允许一个线程修改
1. 计数器示例
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut number = counter.lock().unwrap();
*number += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("最终结果:{}", *counter.lock().unwrap());
}
执行流程:
Arc::clone
↓
每个线程共同拥有 Mutex
↓
lock()
↓
得到 MutexGuard<i32>
↓
通过 DerefMut 修改内部 i32
↓
MutexGuard 离开作用域
↓
自动解锁
MutexGuard 本身也是一种智能指针式的守卫对象。
2. 尽量缩短锁的作用域
不推荐:
let mut data = shared.lock().unwrap(); perform_slow_operation(); data.push(1);
因为执行耗时操作期间,锁一直没有释放。
更合理:
perform_slow_operation();
{
let mut data = shared.lock().unwrap();
data.push(1);
}
或者:
let mut data = shared.lock().unwrap(); data.push(1); drop(data); perform_other_operation();
十、Arc<RwLock<T>>:多读单写
Mutex<T> 同一时间只允许一个线程进入。
RwLock<T> 通常允许:
- 多个读锁同时存在;
- 只有一个写锁存在;
- 写锁存在时不能读取。
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let config = Arc::new(RwLock::new(String::from("development")));
let reader_config = Arc::clone(&config);
let reader = thread::spawn(move || {
let value = reader_config.read().unwrap();
println!("读取配置:{value}");
});
reader.join().unwrap();
{
let mut value = config.write().unwrap();
*value = String::from("production");
}
println!("修改后:{}", config.read().unwrap());
}
适合:
读操作很多,写操作较少
例如:
- 配置缓存;
- 权限规则;
- 路由表;
- 内存字典;
- 共享元数据。
十一、Cell<T>:复制型内部可变性
Cell<T> 和 RefCell<T> 都属于内部可变性工具,但用途不同。
Cell<T> → 直接取出和替换整个值 RefCell<T> → 运行时借用内部值
1. Cell<T> 示例
use std::cell::Cell;
struct Counter {
value: Cell<u32>,
}
impl Counter {
fn increment(&self) {
self.value.set(self.value.get() + 1);
}
fn get(&self) -> u32 {
self.value.get()
}
}
fn main() {
let counter = Counter {
value: Cell::new(0),
};
counter.increment();
counter.increment();
println!("{}", counter.get());
}
虽然:
counter
不是 mut,但内部数字仍可以改变。
Cell<T> 常用于:
Copy
类型,例如:
boolu32usize- 小型枚举
对于 String、Vec<T> 等非 Copy 类型,通常更常用 RefCell<T>。
十二、Cow<T>:写时复制
Cow 的全称是:
Clone On Write 写时复制
它可以保存:
Cow::Borrowed(&T)
或者:
Cow::Owned(T::Owned)
当只读取时,可以继续借用原数据;只有真正需要修改时,才复制为拥有的数据。
示例
use std::borrow::Cow;
fn normalize_name(name: &str) -> Cow<'_, str> {
if name.contains(' ') {
Cow::Owned(name.replace(' ', "_"))
} else {
Cow::Borrowed(name)
}
}
fn main() {
let name1 = normalize_name("Albert");
let name2 = normalize_name("Albert Wen");
println!("{name1}");
println!("{name2}");
println!(
"name1 是否借用:{}",
matches!(name1, Cow::Borrowed(_))
);
println!(
"name2 是否拥有:{}",
matches!(name2, Cow::Owned(_))
);
}
逻辑:
"Albert"
没有空格
↓
直接借用,不复制
"Albert Wen"
需要替换
↓
创建新的 String
适合:
- 字符串清洗;
- 配置处理;
- 序列化;
- 大部分情况不需要修改、少部分情况需要修改的数据。
十三、Pin<P>:固定对象的内存位置
Pin<P> 用于保证一个值在被固定后不会再被移动,除非该类型实现了 Unpin。它主要用于地址敏感的数据结构以及异步编程中的 Future。(doc.rust-lang.org)
常见类型:
Pin<Box<T>> Pin<&mut T>
1. 为什么对象移动可能有问题
一般情况下,Rust 的值可以移动:
fn main() {
let name1 = String::from("hello");
let name2 = name1;
}
对于普通类型没问题。
但某些类型内部可能保存指向自己字段的指针:
对象 ├── data └── ptr ──→ data
如果对象被移动:
旧地址的数据已无效 ptr 仍然指向旧地址
就可能形成悬空指针。
Pin 用来表达:
这个值一旦被固定,就不能再被安全移动。
2. 简单使用
use std::pin::Pin;
fn main() {
let value = Box::pin(String::from("hello"));
let pinned_ref: Pin<&String> = value.as_ref();
println!("{}", pinned_ref.get_ref());
}
日常业务开发中,不需要到处使用 Pin。
最常见的接触场景是异步代码:
Pin<Box<dyn Future<Output = T>>>
因为某些 Future 在执行过程中可能包含自引用状态。
十四、Box<T>、Rc<T>、Arc<T> 的核心区别
| 类型 | 所有者数量 | 可否跨线程 | 默认可修改 | 主要用途 |
|---|---|---|---|---|
Box<T> |
1 | 取决于 T |
可以通过唯一可变借用修改 | 堆分配、递归类型、Trait 对象 |
Rc<T> |
多个 | 不可以 | 不可以直接修改 | 单线程共享所有权 |
Arc<T> |
多个 | 可以 | 不可以直接修改 | 多线程共享所有权 |
RefCell<T> |
1 | 不可以安全共享 | 可以,运行时检查 | 单线程内部可变性 |
Mutex<T> |
1 | 可以 | 加锁后修改 | 多线程互斥修改 |
RwLock<T> |
1 | 可以 | 读写锁控制 | 多读少写 |
Weak<T> |
不拥有 | 取决于是 rc::Weak 还是 sync::Weak |
不直接修改 | 打破循环引用 |
Cow<T> |
借用或拥有 | 取决于内部类型 | 修改时复制 | 减少不必要复制 |
Pin<P> |
由 P 决定 |
由 P 决定 |
受限制 | 固定内存位置 |
十五、常见组合对照
1. Box<T>
单一所有者 不需要共享 数据放到堆上
Box<User>
2. Rc<T>
单线程 多个所有者 只读共享
Rc<Config>
3. Rc<RefCell<T>>
单线程 多个所有者 共享修改
Rc<RefCell<User>>
4. Arc<T>
多线程 多个所有者 只读共享
Arc<AppConfig>
5. Arc<Mutex<T>>
多线程 多个所有者 互斥修改
Arc<Mutex<HashMap<String, User>>>
6. Arc<RwLock<T>>
多线程 多个所有者 多读少写
Arc<RwLock<HashMap<String, Permission>>>
7. Weak<T>
需要引用一个对象 但不希望因此延长对象生命周期
Weak<RefCell<Node>>
十六、一个综合案例:共享应用状态
假设使用 Rust Web 框架开发工单系统,需要在线程之间共享:
- 系统配置;
- 在线用户数量;
- 内存缓存。
可以设计为:
use std::collections::HashMap;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc, RwLock,
};
#[derive(Debug)]
struct AppConfig {
app_name: String,
environment: String,
}
#[derive(Debug)]
struct AppState {
config: AppConfig,
online_users: AtomicUsize,
cache: RwLock<HashMap<String, String>>,
}
fn main() {
let state = Arc::new(AppState {
config: AppConfig {
app_name: String::from("Fuyo Service Desk"),
environment: String::from("production"),
},
online_users: AtomicUsize::new(0),
cache: RwLock::new(HashMap::new()),
});
let request_state = Arc::clone(&state);
request_state
.online_users
.fetch_add(1, Ordering::Relaxed);
{
let mut cache = request_state.cache.write().unwrap();
cache.insert(
String::from("ticket:1001"),
String::from("处理中"),
);
}
{
let cache = state.cache.read().unwrap();
if let Some(status) = cache.get("ticket:1001") {
println!("工单状态:{status}");
}
}
println!("应用名称:{}", state.config.app_name);
println!("运行环境:{}", state.config.environment);
println!(
"在线用户:{}",
state.online_users.load(Ordering::Relaxed)
);
}
这里的设计含义:
Arc<AppState>
│
├── config: AppConfig
│ 只读配置,不需要额外加锁
│
├── online_users: AtomicUsize
│ 简单数字使用原子操作
│
└── cache: RwLock<HashMap<...>>
多线程读写复杂缓存
与把整个状态写成:
Arc<Mutex<AppState>>
相比,这种细粒度设计可以减少不必要的锁竞争。
十七、常见误区
误区一:Rc::clone 会复制内部数据
let rc2 = Rc::clone(&rc1);
通常只是增加引用计数。
而:
let data2 = (*rc1).clone();
才是克隆内部数据。
误区二:Arc<T> 自动让 T 线程安全
不是。
下面不成立:
Arc<RefCell<T>>
Arc 自身的引用计数是线程安全的,但内部类型仍然必须满足线程安全要求。
多线程修改应使用:
Arc<Mutex<T>>
或:
Arc<RwLock<T>>
误区三:RefCell<T> 可以绕过借用规则
不能。
它仍然遵守:
多个不可变借用 或者 一个可变借用
只是从编译期检查改成运行期检查,违反时发生 panic。
误区四:所有共享数据都使用 Arc<Mutex<T>>
这会增加:
- 原子引用计数开销;
- 加锁开销;
- 死锁风险;
- 代码复杂度。
单线程不需要 Arc。
只读共享不需要 Mutex。
简单整数计数可能适合:
AtomicUsize
误区五:Weak<T> 可以直接访问内部数据
不能。
必须先:
weak.upgrade()
然后处理:
Option<Rc<T>>
因为对象可能已经销毁。
误区六:Rust 不会发生内存泄漏
Rust 可以防止大量内存安全问题,但逻辑上的内存泄漏仍可能发生,例如:
Rc循环引用;Arc循环引用;Box::leak;- 永久保存不断增长的数据;
mem::forget。
所以仍然需要正确设计所有权关系。
十八、选择智能指针的判断流程
可以按照下面的顺序判断。
是否需要把数据放到堆上?
│
├─ 是,只有一个所有者
│ └─ Box<T>
│
└─ 需要多个所有者?
│
├─ 单线程
│ │
│ ├─ 只读共享 → Rc<T>
│ └─ 共享修改 → Rc<RefCell<T>>
│
└─ 多线程
│
├─ 只读共享 → Arc<T>
├─ 互斥修改 → Arc<Mutex<T>>
└─ 多读少写 → Arc<RwLock<T>>
额外判断:
存在父子反向引用或循环关系?
└─ 使用 Weak<T>
大部分时候借用,少部分时候修改?
└─ 使用 Cow<T>
对象移动后地址不能改变?
└─ 使用 Pin<Box<T>>
最需要记住的关系是:
Box<T> = 堆分配 + 单一所有权 Rc<T> = 单线程 + 共享所有权 RefCell<T> = 单线程 + 运行时借用检查 Rc<RefCell<T>> = 单线程 + 共享所有权 + 可修改 Arc<T> = 多线程 + 共享所有权 Arc<Mutex<T>> = 多线程 + 共享所有权 + 互斥修改 Weak<T> = 不拥有对象的观察引用
Box::pin 是什么?
Box::pin(value) 的作用是:
把一个值分配到堆上,并返回一个
Pin<Box<T>>,保证这个值之后不能再通过安全 Rust 被移动。
基本形式:
let value: Pin<Box<T>> = Box::pin(data);
需要引入:
use std::pin::Pin;
不过直接调用 Box::pin 时,通常可以不显式写类型,也不必导入 Pin:
fn main() {
let value = Box::pin(String::from("hello"));
println!("{value}");
}
这里 value 的实际类型是:
Pin<Box<String>>
一、Box::new 和 Box::pin 的区别
1. Box::new
let value = Box::new(String::from("hello"));
类型是:
Box<String>
含义:
String存放在堆上;Box拥有这个String;- 可以安全地把内部值取出来或替换;
- 不承诺内部对象永远保持在同一个内存地址。
2. Box::pin
let value = Box::pin(String::from("hello"));
类型是:
Pin<Box<String>>
含义:
String存放在堆上;Box拥有这个String;Pin增加了“不可移动”的约束;- 对于真正需要固定地址的类型,安全代码不能把内部值移走。
简单对比:
Box<T> = 堆分配 + 单一所有权 Pin<Box<T>> = 堆分配 + 单一所有权 + 固定内部值的位置
二、什么叫“值被移动”?
Rust 中的移动不仅仅是修改变量名称。
例如:
fn main() {
let value1 = String::from("hello");
let value2 = value1;
println!("{value2}");
}
这里发生了所有权移动:
value1 → value2
对于普通 String,这种移动没有问题。
但某些特殊对象可能保存了指向自身内部字段的指针。例如:
对象 ├── data: String └── pointer ─────→ data
如果整个对象移动到另一个内存位置:
移动前地址:0x1000 移动后地址:0x2000
内部的 pointer 可能仍然指向旧地址 0x1000,从而失效。
Pin 就是为这种“地址发生变化会导致对象失效”的类型准备的。
三、为什么已经放到堆上,还需要 Pin?
这是最容易产生误解的地方。
很多人会认为:
Box<T>已经把数据放到了堆上,堆地址不是不会变化吗?
通常情况下,移动一个 Box<T> 变量,确实只会移动栈上的指针,堆上的数据通常还在原地址:
栈 堆 Box 指针 ───────────────→ T
但普通 Box<T> 仍允许你通过安全代码把堆中的 T 取出来。
例如:
fn main() {
let boxed = Box::new(String::from("hello"));
let value: String = *boxed;
println!("{value}");
}
这句:
let value = *boxed;
会把内部的 String 从 Box 中移动出来。
也可以替换内部数据:
fn main() {
let mut boxed = Box::new(String::from("hello"));
let old_value = std::mem::replace(
&mut *boxed,
String::from("world"),
);
println!("旧值:{old_value}");
println!("新值:{boxed}");
}
因此,单独使用 Box<T> 并没有提供“内部对象绝不能移动”的语义保证。
使用:
Pin<Box<T>>
后,对于不能随意移动的类型,安全 Rust 就不能轻易把内部的 T 取出来或替换掉。
四、Box::pin 到底固定了谁?
看下面的类型:
Pin<Box<T>>
它固定的是:
Box 指向的 T
不是说外层变量本身完全不能移动。
例如:
fn main() {
let pinned1 = Box::pin(String::from("hello"));
let pinned2 = pinned1;
println!("{pinned2}");
}
这里 pinned1 被移动给了 pinned2,完全合法。
因为移动的是外层的:
Pin<Box<T>>
也就是那个“拥有堆地址的指针对象”。
真正位于堆上的 T 并没有因为这个操作而改变地址:
移动前: pinned1 ─────→ 堆上的 T 移动后: pinned2 ─────→ 同一个堆上的 T
因此准确说法是:
Pin<Box<T>>不禁止移动Box指针本身,而是限制移动Box指向的那个T。
五、Box::pin 的基本使用
use std::pin::Pin;
fn main() {
let value: Pin<Box<String>> =
Box::pin(String::from("hello"));
println!("{value}");
}
读取内部值:
use std::pin::Pin;
fn main() {
let value: Pin<Box<String>> =
Box::pin(String::from("hello"));
let reference: Pin<&String> = value.as_ref();
println!("{}", reference.get_ref());
}
调用过程:
Pin<Box<String>>
│
│ as_ref()
▼
Pin<&String>
│
│ get_ref()
▼
&String
也可以直接调用只需要 &self 的方法:
fn main() {
let value = Box::pin(String::from("hello"));
println!("长度:{}", value.len());
println!("内容:{}", value.as_str());
}
这是因为 Pin<Box<T>>、Box<T> 和 T 之间存在解引用行为。
六、为什么普通 String 使用 Box::pin 好像没区别?
看下面代码:
fn main() {
let mut value = Box::pin(String::from("hello"));
value.push_str(" world");
println!("{value}");
}
它可以正常修改。
这是因为 String 实现了:
Unpin
Unpin 表示:
这个类型即使被固定,也不依赖固定地址;移动它仍然是安全的。
绝大多数普通 Rust 类型都实现了 Unpin,例如:
i32StringVec<T>- 普通业务结构体
- 大多数集合类型
因此,对于 String:
Pin<Box<String>>
通常没有太大实际意义。
甚至可以取回普通的 Box<String>:
use std::pin::Pin;
fn main() {
let pinned = Box::pin(String::from("hello"));
let boxed: Box<String> = Pin::into_inner(pinned);
println!("{boxed}");
}
之所以允许,是因为:
String: Unpin
七、Unpin 是什么?
Pin 和 Unpin 必须放在一起理解。
Unpin 类型
如果一个类型实现了 Unpin,说明:
这个类型不在乎自己的内存地址是否变化。
例如:
String Vec<T> i32 User HashMap<K, V>
对这些类型来说,Pin 基本不会增加太多限制。
!Unpin 类型
如果一个类型没有实现 Unpin,也可以写作:
!Unpin
表示:
一旦被 Pin 固定,就不能再被安全地移动。
常见场景:
- 自引用结构;
- 某些异步
Future; - 状态机内部保存指向自身字段的引用;
- 与底层库交互时依赖稳定地址的对象。
八、如何创建一个 !Unpin 类型?
可以使用:
PhantomPinned
示例:
use std::marker::PhantomPinned;
#[derive(Debug)]
struct NoMove {
name: String,
_pin: PhantomPinned,
}
fn main() {
let value = Box::pin(NoMove {
name: String::from("Albert"),
_pin: PhantomPinned,
});
println!("{}", value.name);
}
PhantomPinned 会让结构体默认不实现 Unpin。
这时:
NoMove: !Unpin
无法安全取出内部对象
下面代码无法编译:
use std::marker::PhantomPinned;
use std::pin::Pin;
struct NoMove {
name: String,
_pin: PhantomPinned,
}
fn main() {
let pinned = Box::pin(NoMove {
name: String::from("Albert"),
_pin: PhantomPinned,
});
let boxed: Box<NoMove> = Pin::into_inner(pinned);
}
原因是:
Pin::into_inner
要求内部类型实现:
Unpin
但是 NoMove 包含 PhantomPinned,因此不满足要求。
这正是 Pin 的作用:
对于
!Unpin类型,阻止安全代码把内部对象移动出来。
九、Pin<Box<T>> 能不能修改内部值?
需要分情况。
情况一:T: Unpin
例如 String:
fn main() {
let mut value = Box::pin(String::from("hello"));
value.push_str(" world");
println!("{value}");
}
可以正常获得可变访问,因为 String: Unpin。
也可以这样:
fn main() {
let mut value = Box::pin(String::from("hello"));
let inner: &mut String = value.as_mut().get_mut();
inner.push_str(" world");
println!("{value}");
}
情况二:T: !Unpin
不能直接获得完整的:
&mut T
因为拿到完整的 &mut T 后,就可能使用 mem::replace 等操作把它移走。
例如:
use std::marker::PhantomPinned;
struct NoMove {
name: String,
_pin: PhantomPinned,
}
fn main() {
let mut value = Box::pin(NoMove {
name: String::from("Albert"),
_pin: PhantomPinned,
});
// 无法编译,因为 NoMove 没有实现 Unpin
// let inner: &mut NoMove = value.as_mut().get_mut();
}
不过仍然可以安全读取:
fn main() {
// 创建过程略
// value.as_ref().get_ref() 可以获得 &NoMove
}
注意:
不可移动 ≠ 完全不可修改
某些字段仍可能在满足固定投影规则时修改,只是不能随意移动整个对象。实际项目通常借助 pin-project 等库安全处理固定字段。
十、Box::pin 与 Box::new 加 Box::into_pin
下面两种写法基本等价:
let value = Box::pin(String::from("hello"));
以及:
let boxed = Box::new(String::from("hello"));
let value = Box::into_pin(boxed);
也就是:
Box::pin(value)
可以理解为更方便的简写:
Box::into_pin(Box::new(value))
十一、最常见的使用场景:异步 Future
Rust 的 async 代码会被编译器转换成一个状态机。
例如:
async fn load_data() -> String {
String::from("data")
}
概念上会被转换成类似:
Future 状态机 ├── 当前执行到哪个 await ├── 局部变量 ├── 等待中的 Future └── 某些内部状态之间可能存在地址依赖
在轮询 Future 时,标准接口是:
trait Future {
type Output;
fn poll(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Self::Output>;
}
注意 poll 的接收者不是:
&mut self
而是:
self: Pin<&mut Self>
这是因为某些 Future 在开始执行之后不能再移动。
将 Future 固定到堆上
use std::future::Future;
use std::pin::Pin;
fn create_future()
-> Pin<Box<dyn Future<Output = i32>>>
{
Box::pin(async {
100
})
}
这里:
Box::pin(async { 100 })
返回:
Pin<Box<某个匿名 Future 类型>>
再通过动态派发统一为:
Pin<Box<dyn Future<Output = i32>>>
这种写法在异步 Rust 中非常常见。
十二、为什么返回异步 Trait 对象时经常看到 Pin<Box<dyn Future>>?
假设需要根据条件返回不同的异步逻辑:
use std::future::Future;
use std::pin::Pin;
fn execute(
use_cache: bool,
) -> Pin<Box<dyn Future<Output = String>>> {
if use_cache {
Box::pin(async {
String::from("从缓存读取")
})
} else {
Box::pin(async {
String::from("从数据库读取")
})
}
}
这里使用了三个关键概念:
Box → 在堆上保存大小可能不同的 Future dyn Future → 通过 Trait 对象统一不同的 Future 类型 Pin → 保证 Future 在执行过程中不会被非法移动
因此:
Pin<Box<dyn Future<Output = String>>>
可以理解成:
一个放在堆上、地址固定、通过动态派发调用、最终输出
String的异步任务。
十三、在实际业务开发中什么时候会遇到?
1. 返回动态 Future
type BoxFuture<T> =
Pin<Box<dyn Future<Output = T> + Send>>;
实际项目中经常定义:
use std::future::Future;
use std::pin::Pin;
type BoxFuture<'a, T> =
Pin<Box<dyn Future<Output = T> + Send + 'a>>;
2. 保存一组不同的异步任务
use std::future::Future;
use std::pin::Pin;
type Task =
Pin<Box<dyn Future<Output = ()> + Send>>;
fn main() {
let _tasks: Vec<Task> = vec![
Box::pin(async {
println!("任务一");
}),
Box::pin(async {
println!("任务二");
}),
];
}
每个 async 块都有不同的匿名类型,所以通过:
Pin<Box<dyn Future<Output = ()> + Send>>
统一保存。
3. Stream
异步流中也经常看到:
Pin<Box<dyn Stream<Item = T> + Send>>
原因和 Future 相同:
- 动态类型;
- 堆分配;
- 需要固定地址。
4. 自引用数据结构
例如对象中的某个字段指向另一个字段:
SelfReferential ├── text: String └── pointer ─────→ text
这类结构一旦初始化完成,就不能再随意移动。
不过手写安全的自引用结构比较复杂,实际项目一般使用成熟库,而不是直接大量编写 unsafe。
十四、一个地址演示
下面可以观察堆上数据的地址:
fn main() {
let pinned1 = Box::pin(String::from("hello"));
let address1 = pinned1.as_ref().get_ref()
as *const String;
let pinned2 = pinned1;
let address2 = pinned2.as_ref().get_ref()
as *const String;
println!("移动前地址:{address1:p}");
println!("移动后地址:{address2:p}");
assert_eq!(address1, address2);
}
虽然:
pinned1
移动成了:
pinned2
但是它们指向的堆上 String 地址没有变化。
需要注意:这个例子体现了 Box 堆分配的地址稳定特征,而 Pin 进一步把这种特征提升为针对被固定对象的 API 安全保证。
十五、Box::pin 不等于绝对不可移动
Pin 的保证需要准确理解。
它不是说:
内存从此在任何情况下都不可能变化
而是说:
对于正确实现并遵守
Pin契约的代码,不能通过安全 Rust 移动被固定的!Unpin值。
仍然可能通过以下方式破坏约束:
- 错误编写的
unsafe代码; - 错误实现固定投影;
- 错误使用
Pin::get_unchecked_mut; - 对底层内存执行不符合契约的操作。
所以 Pin 本质上是一套由 Rust 类型系统和 unsafe 契约共同维护的保证。
十六、什么时候应该使用 Box::pin?
适合使用:
- API 明确要求
Pin<Box<T>>; - 需要返回
Pin<Box<dyn Future<...>>>; - 需要保存动态异步任务;
- 处理
!Unpin类型; - 构建地址敏感或自引用结构;
- 与要求稳定地址的底层库交互。
通常不需要使用:
let number = Box::pin(100);
let name = Box::pin(String::from("Albert"));
let users = Box::pin(Vec::<String>::new());
因为这些普通类型都不依赖固定地址,使用:
Box::new(...)
通常就足够了。
十七、一句话记忆
Box::new(value) = 把 value 放到堆上 Box::pin(value) = 把 value 放到堆上,并返回 Pin<Box<T>>, 为需要固定地址的类型提供“不能再安全移动”的保证
最典型的代码是:
use std::future::Future;
use std::pin::Pin;
fn create_task()
-> Pin<Box<dyn Future<Output = String> + Send>>
{
Box::pin(async {
String::from("任务完成")
})
}
其中:
Box 解决堆分配和动态大小 dyn Future 解决不同 Future 类型的统一 Pin 解决 Future 执行期间不能移动的问题