Rust-智能指针

97 阅读9分钟

智能指针在 Rust 里非常重要,因为它们能够帮助管理内存、实现特定的功能,例如自动内存回收、引用计数等。Rust 标准库中提供了多种智能指针,常见的有 Box<T>Rc<T>Arc<T> 和 RefCell<T>

智能指针核心功能适用场景线程安全可变性方式
Box<T>堆存储、类型擦除递归类型、大数据转移、trait 对象无关直接通过所有权修改
Rc<T>单线程多所有者共享单线程内共享数据不安全需配合 RefCell
Arc<T>多线程原子计数共享跨线程共享数据安全需配合 Mutex/RwLock
RefCell运行时借用检查、内部可变性编译时不可变外壳下修改内部数据不安全borrow_mut() 运行时检查
Weak<T>弱引用、打破循环解决 Rc/Arc 循环引用同原指针无(需 upgrade 后访问)

Box<T>

Box<T> 实现了 Deref 和 Drop 这两个 trait,这使得它具备以下特点:

  • 当 Box<T> 离开作用域时,会自动释放堆上的数据,这一过程遵循 Rust 的所有权规则。
  • 可以像使用普通引用一样使用 Box<T>,这得益于 Deref trait 的实现。

创建和使用

fn main() {
    // 在堆上分配一个 i32 类型的值
    let boxed_num = Box::new(42);
    println!("Value: {}", boxed_num); // 可以直接使用,无需显式解引用

    // Box 实现了 Deref,所以可以直接调用 i32 的方法
    println!("Is even: {}", boxed_num.is_even());

    // 离开作用域时,内存会自动释放
}

常用方法

Box::new在堆上分配并初始化一个值
Box::into_inner解构 Box,返回内部值
Box::leak泄露内存,返回静态生命周期引用
Box::into_raw将 Box 转换为裸指针,需手动管理内存
Box::from_raw从裸指针重新构建 Box,恢复自动内存管理
Box::as_ref获取内部值的不可变引用
Box::as_mut获取内部值的可变引用
Box::pin创建固定在内存中的 Box,用于异步编程

应用

存储在编译时大小未知的数据

Rust 要求在编译时明确知道栈上数据的大小,而递归类型在编译时无法确定大小,这时就可以使用 Box<T> 来解决这个问题。

enum List {
    Cons(i32, Box<List>), // Box 的大小是已知的(一个指针的大小)
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    println!("List created."); // 内存会在 list 离开作用域时自动释放
}

转移大型数据的所有权

将大型数据存储在堆上,通过 Box<T> 转移所有权,能够避免进行代价高昂的数据复制操作。

struct LargeData {
    data: [u8; 1000],
}

fn process_data(data: Box<LargeData>) {
    // 处理数据
    println!("Data processed.");
}

fn main() {
    let large_data = Box::new(LargeData { data: [0; 1000] });
    process_data(large_data); // 所有权转移,没有复制整个数组
}

实现 trait 对象

Box<T> 可用于创建 trait 对象,从而实现动态分发。

trait Draw {
    fn draw(&self);
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Draw for Circle {
    fn draw(&self) { println!("Drawing a circle."); }
}

impl Draw for Square {
    fn draw(&self) { println!("Drawing a square."); }
}

fn main() {
    let shapes: Vec<Box<dyn Draw>> = vec![
        Box::new(Circle { radius: 1.0 }),
        Box::new(Square { side: 2.0 }),
    ];

    for shape in shapes {
        shape.draw(); // 动态调用对应的 draw 方法
    }
}

Weak<T>

在 Rust 中,Weak<T> 是一种特殊的智能指针,用于实现对 Rc<T>(引用计数)管理的值的弱引用。与 Rc<T> 不同,Weak<T> 不会增加引用计数,因此不会阻止值被释放。这使得 Weak<T> 成为打破循环引用、实现安全双向引用的关键工具。下面详细解析 Weak<T> 的核心概念、应用场景和使用方法。

1. 核心概念与特性

  • 弱引用Weak<T> 不拥有值,不增加引用计数,允许值在没有 Rc<T> 引用时被释放。
  • 不可直接解引用:不能通过 Weak<T> 直接访问值,需先通过 upgrade() 方法尝试将其提升为 Option<Rc<T>>
  • 自动失效:当被引用的值被释放后,Weak<T> 自动失效(upgrade() 返回 None)。
  • 生命周期Weak<T> 的生命周期可以长于被引用的值,不会阻止值的释放。

 创建与使用 Weak<T>

 从 Rc<T> 创建 Weak<T>

通过 Rc::downgrade() 方法将 Rc<T> 转换为 Weak<T>

use std::rc::{Rc, Weak};

fn main() {
    let strong = Rc::new(42); // 强引用计数: 1
    
    // 创建弱引用
    let weak: Weak<i32> = Rc::downgrade(&strong);
    
    // 尝试提升为强引用
    if let Some(strong_ref) = weak.upgrade() {
        println!("Value: {}", *strong_ref); // 输出: 42
    }
    
    // strong 离开作用域,值被释放
    drop(strong);
    
    // 此时提升失败
    assert!(weak.upgrade().is_none());
}

 获取弱引用计数

通过 Weak::strong_count() 和 Weak::weak_count() 查看引用计数:

let strong = Rc::new(42);
let weak = Rc::downgrade(&strong);

println!("Strong count: {}", Rc::strong_count(&strong)); // 1
println!("Weak count: {}", Rc::weak_count(&strong));     // 1

 典型应用场景

 打破循环引用

在双向引用结构(如树的父 - 子关系)中,使用 Weak<T> 避免循环:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Node {
    value: i32,
    parent: RefCell<Option<Weak<Node>>>, // 弱引用父节点
    children: RefCell<Vec<Rc<Node>>>,    // 强引用子节点
}

fn main() {
    let parent = Rc::new(Node {
        value: 1,
        parent: RefCell::new(None),
        children: RefCell::new(Vec::new()),
    });
    
    let child = Rc::new(Node {
        value: 2,
        parent: RefCell::new(Some(Rc::downgrade(&parent))),
        children: RefCell::new(Vec::new()),
    });
    
    // 父节点持有子节点的强引用
    parent.children.borrow_mut().push(Rc::clone(&child));
    
    // 当 parent 离开作用域时,即使 child 仍引用 parent,parent 也会被释放
}

 缓存与延迟加载

弱引用可用于实现缓存,当值存在时使用,不存在时重新加载:

struct CachedData {
    data: Weak<RefCell<Vec<u8>>>,
}

impl CachedData {
    fn get_or_load(&self) -> Rc<RefCell<Vec<u8>>> {
        if let Some(data) = self.data.upgrade() {
            data // 缓存存在
        } else {
            let new_data = Rc::new(RefCell::new(self.load_data()));
            self.data = Rc::downgrade(&new_data);
            new_data
        }
    }
    
    fn load_data(&self) -> Vec<u8> {
        // 从磁盘或网络加载数据
        vec![1, 2, 3]
    }
}

Rc<T>

在 Rust 中,Rc<T>(Reference Counting)是一种智能指针,用于实现多所有权(multiple ownership)模式。它通过引用计数跟踪有多少个所有者共享同一个值,并在最后一个所有者离开作用域时自动释放内存。下面详细介绍 Rc<T> 的核心概念、使用场景、方法及注意事项。

 基本概念与特性

  • 多所有权Rc<T> 允许一个值有多个所有者,通过克隆(clone())增加引用计数。
  • 只读共享Rc<T> 只提供不可变引用,若需可变共享,需结合 RefCell<T> 使用。
  • 单线程Rc<T> 不是线程安全的,多线程环境需使用 Arc<T>
  • 自动释放:当引用计数降为 0 时,值被自动删除。

 创建与克隆 Rc<T>

use std::rc::Rc;

fn main() {
    // 创建 Rc 包装的值
    let a = Rc::new(String::from("hello"));
    
    // 克隆 Rc(增加引用计数,而非深拷贝值)
    let b = Rc::clone(&a);
    let c = a.clone(); // 等价写法
    
    println!("a: {}, b: {}, c: {}", a, b, c);
    // 引用计数:3
} // a, b, c 离开作用域,引用计数降为 0,内存释放

 核心方法

 Rc::new(value)

创建一个新的 Rc<T>

 Rc::clone(&rc)

克隆 Rc<T>,增加引用计数(浅拷贝)。

 Rc::strong_count(&rc)

返回当前引用计数(用于调试)。

let a = Rc::new(42);
let b = a.clone();
println!("Strong count: {}", Rc::strong_count(&a)); // 输出: 2

 Rc::into_inner(rc)

将 Rc<T> 转换为内部值,但仅当引用计数为 1 时有效。

let a = Rc::new(42);
if let Ok(val) = Rc::into_inner(a) {
    println!("Value: {}", val); // 成功获取内部值
}

 典型应用场景

 共享不可变数据

多个部分的代码需要读取同一数据,且无需修改。

use std::rc::Rc;

struct Node {
    value: i32,
    parent: Option<Rc<Node>>, // 父节点共享所有权
}

fn main() {
    let parent = Rc::new(Node {
        value: 10,
        parent: None,
    });
    
    let child = Node {
        value: 20,
        parent: Some(Rc::clone(&parent)), // 共享 parent 的所有权
    };
    
    println!("Child's parent value: {}", child.parent.unwrap().value);
}

 构建图结构

图中的节点可能被多个其他节点引用。

use std::rc::Rc;

struct GraphNode {
    data: i32,
    neighbors: Vec<Rc<GraphNode>>,
}

fn main() {
    let node1 = Rc::new(GraphNode { data: 1, neighbors: vec![] });
    let node2 = Rc::new(GraphNode { 
        data: 2, 
        neighbors: vec![Rc::clone(&node1)] 
    });
    
    // node1 被 node2 引用,共享所有权
}

 线程安全注意事项

  • Rc<T> 不是线程安全的,不能跨线程使用。
  • 多线程环境需使用 Arc<T>(Atomic Reference Counting)替代。

RefCell<T>

在 Rust 中,RefCell<T> 是一种用于实现内部可变性(Interior Mutability)的智能指针。与 Rust 常规的 “不可变引用 + 唯一可变引用” 规则不同,RefCell<T> 允许在不可变引用存在的情况下修改数据,通过运行时借用检查替代编译时检查。下面详细介绍 RefCell<T> 的核心概念、使用场景、方法及注意事项。

 基本概念与特性

  • 内部可变性:即使类型被声明为不可变(let x = RefCell::new(...)),仍可通过 RefCell<T> 修改其内部值。
  • 运行时检查:借用规则(同一时间只能有一个可变引用或多个不可变引用)在运行时检查,若违反规则会触发 panic
  • 单线程RefCell<T> 不是线程安全的,多线程环境需使用 Mutex<T> 或 RwLock<T>
  • 与 Rc<T> 结合:常用于实现多所有权下的可变数据(如树结构的父 - 子关系)。

 核心方法

 RefCell::new(value)

创建一个新的 RefCell<T>

 borrow()

获取不可变引用(返回 Ref<T>)。若当前存在可变引用,会触发 panic

use std::cell::RefCell;

let cell = RefCell::new(5);
let borrowed = cell.borrow(); // Ref<i32>
println!("Borrowed: {}", *borrowed);

 borrow_mut()

获取可变引用(返回 RefMut<T>)。若当前存在任何引用(可变或不可变),会触发 panic

let cell = RefCell::new(5);
{
    let mut borrowed = cell.borrow_mut(); // RefMut<i32>
    *borrowed += 10;
} // 可变引用在此作用域结束后释放

 try_borrow() 和 try_borrow_mut()

非 panic 版本的借用方法,返回 Result

let cell = RefCell::new(5);
if let Ok(mut b) = cell.try_borrow_mut() {
    *b += 10;
}

 典型应用场景

 在不可变数据结构中实现可变状态

use std::cell::RefCell;

struct Person {
    name: String,
    secret_notes: RefCell<Vec<String>>, // 允许在不可变的 Person 实例中修改
}

impl Person {
    fn add_secret(&self, note: String) {
        self.secret_notes.borrow_mut().push(note);
    }
}

fn main() {
    let alice = Person {
        name: "Alice".to_string(),
        secret_notes: RefCell::new(Vec::new()),
    };
    
    alice.add_secret("Likes chocolate".to_string()); // 即使 alice 是不可变的
    println!("Notes: {:?}", alice.secret_notes.borrow());
}

 实现观察者模式

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

type Observer = Rc<dyn Fn()>;

struct Observable {
    observers: RefCell<Vec<Observer>>,
}

impl Observable {
    fn add_observer(&self, observer: Observer) {
        self.observers.borrow_mut().push(observer);
    }
    
    fn notify(&self) {
        for observer in self.observers.borrow().iter() {
            observer();
        }
    }
}

fn main() {
    let observable = Observable { observers: RefCell::new(Vec::new()) };
    
    let counter = Rc::new(RefCell::new(0));
    let observer = Rc::new(move || {
        *counter.borrow_mut() += 1;
    });
    
    observable.add_observer(observer);
    observable.notify();
    
    println!("Counter: {}", counter.borrow()); // 输出: 1
}

 与 Rc<T> 结合构建双向引用结构

use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Node {
    value: i32,
    parent: RefCell<Option<Weak<Node>>>, // 弱引用,避免循环
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let parent = Rc::new(Node {
        value: 1,
        parent: RefCell::new(None),
        children: RefCell::new(vec![]),
    });
    
    let child = Rc::new(Node {
        value: 2,
        parent: RefCell::new(Some(Rc::downgrade(&parent))),
        children: RefCell::new(vec![]),
    });
    
    parent.children.borrow_mut().push(Rc::clone(&child));
    
    // 通过子节点访问父节点
    if let Some(p) = child.parent.borrow().as_ref().and_then(|wp| wp.upgrade()) {
        println!("Parent value: {}", p.value);
    }
}

 运行时借用检查规则

  • 不可变借用:可多次调用 borrow(),返回多个 Ref<T>
  • 可变借用:只能调用一次 borrow_mut(),返回唯一的 RefMut<T>,且此时不能存在任何不可变借用。
  • 违反规则:若违反上述规则,会在运行时触发 panic

 性能考量

  • 开销RefCell<T> 的借用检查需要维护内部状态(借用计数),比普通引用略慢。
  • 适用场景:适用于编译时无法确定借用关系,但运行时很少违反借用规则的场景。