一文搞懂 Rust 智能指针:Box、Rc、Arc、RefCell

AI 概述
Rust所有权规则严格,智能指针可解决多场景问题:Box用于堆上分配,解决递归类型、大块头数据和trait对象存储;Rc单线程下实现引用计数,方便多人共享数据;Arc是线程安全版的Rc,用于多线程共享数据;RefCell提供内部可变性,打破常规,允许在不可变引用下修改数据;Mutex用于多线程下的内部可变性。常见组合有Rc、Arc。使用时需注意线程安全、借用规则,避免常见错误。
目录
文章目录隐藏
  1. 一、Box:堆上安家
  2. 二、Rc:引用计数(单线程)
  3. 三、Arc:原子引用计数(多线程)
  4. 四、RefCell:内部可变性
  5. 五、组合使用:Rc<RefCell>
  6. 六、Mutex:多线程内部可变性
  7. 七、怎么选?
  8. 八、常见错误与修复
  9. 九、总结

一文搞懂详解 Rust 智能指针:Box、Rc、Arc、RefCell

Rust 的所有权规则很严。严到什么程度?编译期就跟你急。

但有些场景,它确实不太方便:

  • 多个地方要用同一份数据;
  • 运行时才确定谁该持有;
  • 拿到了”不可变引用”,却想改点东西。

智能指针就是来解决这些麻烦的。

这篇文章会讲四种:

  • Box<T> – 堆上分配;
  • Rc<T> – 引用计数,单线程用;
  • Arc<T> – 原子引用计数,多线程用;
  • RefCell<T> – 内部可变性。

一、Box:堆上安家

1.1 什么是 Box?

Box<T>做的事情很简单:把数据扔到堆上去。

1.2 什么时候用 Box?

场景 1:递归类型,编译器犯难

// 错误:编译时不知道 List 的大小
enum List {
    Cons(i32, List),  // List 的大小取决于自身,无限递归
    Nil,
}
// 正确:用 Box 存储递归类型
enum List {
    Cons(i32, Box<List>),  // Box 大小固定(指针大小)
    Nil,
}

fn main() {
    let list = List::Cons(1,
        Box::new(List::Cons(2,
            Box::new(List::Cons(3,
                Box::new(List::Nil))))));
}

场景 2:大块头数据

// 栈空间有限,放个大数组可能爆
struct LargeData {
    data: [i32; 1000000],
}

// 扔到堆上去
let data: Box<LargeData> = Box::new(LargeData {
    data: [0; 1000000],
});

场景 3: trait 对象

// 存储不同类型的 Trait 对象
trait Animal {
    fn speak(&self);
}

struct Dog;
impl Animal for Dog {
    fn speak(&self) { println!("Woof!"); }
}

struct Cat;
impl Animal for Cat {
    fn speak(&self) { println!("Meow!"); }
}

// 用 Box 存储 Trait 对象
let animals: Vec<Box<dyn Animal>> = vec![
    Box::new(Dog),
    Box::new(Cat),
];

for animal in animals {
    animal.speak();
}

1.3 解引用

let b = Box::new(5);
println!("b = {}", b);  // 自动解引用

let x = *b;  // 手动也行
println!("x = {}", x);

// 用完自动释放,不用你操心

二、Rc:引用计数(单线程)

2.1 什么是 Rc?

Rc<T> = Reference Counting

特点:

  • 多个地方共享同一份数据;
  • 运行时数着有多少人在用;
  • 没人用时,数据才释放;
  • 单线程专用。

2.2 什么时候用 Rc?

场景:好几个人要读同一份数据

// 错误:所有权转移
let s = String::from("hello");
let a = s;
let b = s;  // s 已经转移给 a 了
// 用 Rc 共享数据
use std::rc::Rc;

let s = Rc::new(String::from("hello"));
let a = Rc::clone(&s);  // 增加引用计数
let b = Rc::clone(&s);  // 再增加引用计数

println!("s = {}", s);
println!("a = {}", a);
println!("b = {}", b);

println!("引用计数:{}", Rc::strong_count(&s));  // 输出 3

2.3 引用计数怎么变化

use std::rc::Rc;

{
    let a = Rc::new(5);
    println!("count = {}", Rc::strong_count(&a));  // 1
    
    {
        let b = Rc::clone(&a);
        println!("count = {}", Rc::strong_count(&a));  // 2
        
        {
            let c = Rc::clone(&a);
            println!("count = {}", Rc::strong_count(&a));  // 3
        }  // c 没了,count = 2
        
        println!("count = {}", Rc::strong_count(&a));  // 2
    }  // b 没了,count = 1
    
    println!("count = {}", Rc::strong_count(&a));  // 1
}  // a 也没了,数据释放

2.4 常见用法:图结构

use std::rc::Rc;

enum Node {
    Value(i32),
    Link(Rc<Node>, Rc<Node>),
}

fn main() {
    let leaf1 = Rc::new(Node::Value(1));
    let leaf2 = Rc::new(Node::Value(2));
    
    // 多个节点可以共享同一个子节点
    let parent = Node::Link(
        Rc::clone(&leaf1),
        Rc::clone(&leaf2),
    );
    
    let sibling = Node::Link(
        Rc::clone(&leaf1),  // 共享 leaf1
        Rc::new(Node::Value(3)),
    );
}

三、Arc:原子引用计数(多线程)

3.1 什么是 Arc?

Arc<T> = Atomic Reference Counting

特点:

  • 跟 Rc 一样,多个所有者;
  • 线程安全,多线程随便用;
  • 原子操作,稍慢一点。

3.2 什么时候用 Arc?

场景:多个线程要共享数据

use std::sync::Arc;
use std::thread;

let data = Arc::new(vec![1, 2, 3]);

let mut handles = vec![];

for i in 0..3 {
    let data_clone = Arc::clone(&data);
    let handle = thread::spawn(move || {
        println!("线程 {} 看到:{:?}", i, data_clone);
    });
    handles.push(handle);
}

for handle in handles {
    handle.join().unwrap();
}

3.3 Arc vs Rc

特性 Rc Arc
线程安全
性能 略慢(原子操作)
使用场景 单线程 多线程

规则:

  • 单线程用Rc
  • 多线程用Arc

四、RefCell:内部可变性

4.1 什么是内部可变性?

说白了:拿到”不可变引用”,照改不误。

Rust 常规操作:

  • &T – 只读,不能改;
  • &mut T – 可写。

RefCell<T>打破常规:

  • 拿着&RefCell<T>也能改里面;
  • 借用检查挪到运行时做。

4.2 什么时候用 RefCell?

场景 1:明明拿到了”不可变”却想改

// 错误:不可变借用下不能修改
struct Data {
    value: i32,
}

fn modify(data: &Data) {
    data.value = 42;  // 错误
}
// 用 RefCell
use std::cell::RefCell;

struct Data {
    value: RefCell<i32>,
}

fn modify(data: &Data) {
    *data.value.borrow_mut() = 42;  // 可以修改
}

fn main() {
    let data = Data {
        value: RefCell::new(0),
    };
    
    modify(&data);  // 传递不可变引用
    println!("value = {}", data.value.borrow());  // 输出 42
}

场景 2:Rc + RefCell 组合

use std::rc::Rc;
use std::cell::RefCell;

// 多人共享 + 还能改
let data = Rc::new(RefCell::new(vec![1, 2, 3]));

let a = Rc::clone(&data);
let b = Rc::clone(&data);

// 都能改
a.borrow_mut().push(4);
b.borrow_mut().push(5);

println!("{:?}", data.borrow());  // [1, 2, 3, 4, 5]

4.3 借用规则

运行时检查:

use std::cell::RefCell;

let data = RefCell::new(5);

{
    let r1 = data.borrow();    // 不可变借用
    let r2 = data.borrow();    // 可以有多个不可变借用
    println!("r1 = {}, r2 = {}", r1, r2);
}  // 借用释放

{
    let r1 = data.borrow_mut();  // 可变借用
    // let r2 = data.borrow();   // 运行时 panic!
    *r1 = 10;
}  // 借用释放

注意:违规则 panic,不会等到编译。

use std::cell::RefCell;

let data = RefCell::new(5);

// 不可变借用
let r = data.borrow();  // 返回 Ref<i32>
println!("{}", *r);

// 可变借用
let mut w = data.borrow_mut();  // 返回 RefMut<i32>
*w = 10;

// 尝试借用(不 panic)
match data.try_borrow() {
    Ok(r) => println!("{}", *r),
    Err(e) => println!("借用失败:{}", e),
}

五、组合使用:Rc<RefCell>

5.1 为什么要组合?

  • Rc 解决多人共享;
  • RefCell 解决能改;
  • 加一起:既能共享又能改。

5.2 实战示例

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Person {
    name: String,
    friends: RefCell<Vec<Rc<Person>>>,
}

fn main() {
    let alice = Rc::new(Person {
        name: String::from("Alice"),
        friends: RefCell::new(vec![]),
    });
    
    let bob = Rc::new(Person {
        name: String::from("Bob"),
        friends: RefCell::new(vec![]),
    });
    
    // 互相添加为好友
    alice.friends.borrow_mut().push(Rc::clone(&bob));
    bob.friends.borrow_mut().push(Rc::clone(&alice));
    
    println!("{:?} 的朋友:", alice.name);
    for friend in alice.friends.borrow() {
        println!("  - {}", friend.name);
    }
}

六、Mutex:多线程内部可变性

6.1 什么是 Mutex?

Mutex<T> = Mutual Exclusion(互斥锁)

特点:

  • 多线程下的内部可变性;
  • 同一时刻只准一个线程访问;
  •  配合 Arc 一起用。

6.2 什么时候用 Mutex?

场景:多线程要共享还能改的数据

use std::sync::{Arc, Mutex};
use std::thread;

let data = Arc::new(Mutex::new(vec![1, 2, 3]));

let mut handles = vec![];

for i in 0..3 {
    let data_clone = Arc::clone(&data);
    let handle = thread::spawn(move || {
        let mut nums = data_clone.lock().unwrap();
        nums.push(i);
        println!("线程 {} 添加元素", i);
    });
    handles.push(handle);
}

for handle in handles {
    handle.join().unwrap();
}

println!("结果:{:?}", data.lock().unwrap());

6.3 Mutex vs RefCell

特性 RefCell Mutex
检查时机 运行时 运行时
线程安全
性能 较慢(锁开销)
错误处理 panic 返回 Result

七、怎么选?

决策树

要堆上分配吗?
├─ 是 → 要多人共享吗?
│      ├─ 否 → Box<T>
│      └─ 是 → 单线程?
│              ├─ 是 → Rc<T>
│              └─ 否 → Arc<T>
└─ 否 → 要能改数据吗?
       ├─ 单线程 → RefCell<T>
       └─ 多线程 → Mutex<T>

常见组合

组合 用途
Box<T> 堆上分配,单一所有者
Rc<T> 单线程多所有者共享
Arc<T> 多线程多所有者共享
RefCell<T> 单线程内部可变性
Mutex<T> 多线程内部可变性
Rc<RefCell<T>> 单线程多所有者 + 可变
Arc<Mutex<T>> 多线程多所有者 + 可变

八、常见错误与修复

错误 1:Rc 用于多线程

use std::rc::Rc;
use std::thread;

let data = Rc::new(5);

thread::spawn(move || {
    println!("{}", data);  // 编译错误:Rc 不能在线程间传递
});

修复:

use std::sync::Arc;
use std::thread;

let data = Arc::new(5);

thread::spawn(move || {
    println!("{}", data);  // Arc 可以在线程间传递
});

错误 2:RefCell 借用冲突

use std::cell::RefCell;

let data = RefCell::new(5);

let r1 = data.borrow();
let r2 = data.borrow_mut();  // 运行时 panic!

修复:

use std::cell::RefCell;

let data = RefCell::new(5);

{
    let r1 = data.borrow();
    println!("{}", *r1);
}  // r1 释放

let r2 = data.borrow_mut();  // 现在可以了
*r2 = 10;

错误 3:忘记解引用

use std::rc::Rc;

let data = Rc::new(5);
println!("{}", data);  // Rc 实现了 Display
println!("{}", *data);  // 手动解引用也可以

九、总结

核心要点

  1. Box<T> – 堆上分配;
  2. Rc<T> – 单线程多人共享;
  3. Arc<T> – 多线程多人共享;
  4. RefCell<T> – 运行时检查借用;
  5. Mutex<T> – 多线程加锁改数据。

快速选择

需求 选哪个
堆上分配 Box<T>
递归类型 Box<T>
单线程共享 Rc<T>
多线程共享 Arc<T>
单线程能改 RefCell<T>
多线程能改 Mutex<T>
单线程共享+能改 Rc<RefCell<T>>
多线程共享+能改 Arc<Mutex<T>>

以上关于一文搞懂 Rust 智能指针:Box、Rc、Arc、RefCell的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。

「点点赞赏,手留余香」

9

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » 一文搞懂 Rust 智能指针:Box、Rc、Arc、RefCell

发表回复