不灭的焱

革命尚未成功,同志仍须努力 下载Java21

作者:AlbertWen  添加时间:2026-07-12 08:53:07  修改时间:2026-07-22 11:59:17  分类:01.Rust编程  编辑

目录

Rust 智能指针详解

Rust 中的“智能指针”不是一种特殊语法,而是一类像指针一样使用、但额外携带管理能力的结构体

普通引用 &T 只负责借用数据;智能指针通常还可以负责:

  • 拥有数据;
  • 在堆上分配数据;
  • 统计引用数量;
  • 控制共享与可变访问;
  • 在离开作用域时自动释放资源;
  • 在线程之间安全共享数据。

Rust 官方重点介绍的智能指针包括 Box<T>Rc<T>RefCell<T>,并通过 DerefDrop 实现类似普通引用的访问及自动清理。(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();
}

因为 EmailNotificationSmsNotification 是不同类型,函数不能直接根据条件返回两种具体类型。

但是它们都实现了:

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);

它通常只会:

  1. 增加强引用计数;
  2. 复制指向同一块分配的指针。

不会复制内部的整个对象。

推荐写法:

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> 的引用计数更新不是线程安全的,不实现线程传递所需的 SendSync。跨线程共享应该使用 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 不会阻止内部值被释放,常用于避免 RcArc 形成无法释放的循环引用。(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

类型,例如:

  • bool
  • u32
  • usize
  • 小型枚举

对于 StringVec<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::newBox::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;

会把内部的 StringBox 中移动出来。

也可以替换内部数据:

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,例如:

  • i32
  • String
  • Vec<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 是什么?

PinUnpin 必须放在一起理解。

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::pinBox::newBox::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

适合使用:

  1. API 明确要求 Pin<Box<T>>
  2. 需要返回 Pin<Box<dyn Future<...>>>
  3. 需要保存动态异步任务;
  4. 处理 !Unpin 类型;
  5. 构建地址敏感或自引用结构;
  6. 与要求稳定地址的底层库交互。

通常不需要使用:

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 执行期间不能移动的问题