Skip to content

智能指针

rust 最基础的指针就是引用。

智能指针其实是一种数据结构,除了提供数据的使用还提供额外的能力。String 和 Vec<T> 等类型其实也都属于智能指针

标准库提供的几个常用智能指针:

  • Box<T>,用于在堆上分配值,使用方法跟引用没区别
  • Rc<T>,一个引用计数类型,其数据可以有多个所有者;当引用数为 0 时,指针和其指向的内存就会被释放
  • RefCell<T> (注意,这不是指针),一个用于实现内部可变的容器。通过它可以访问Ref<T> (运行时的不可变引用)和 RefMut<T>(运行时的可变引用),这俩才是指针(引用)。

Box<T>

Box<T> 的用法跟rust本身的借用、解引用差不多,栈上的指针指向堆上存放数据,而且是所有权唯一的。

rust
fn main() {
    // 语法:Box::new(值)
    let b = Box::new(5);	// 变量 b 是一个指向堆上整数 5 的指针
    
    // 直接打印 Box 时,Rust 会自动解引用并调用内部值的 Display 实现
    println!("b = {}", b);           // 输出: b = 5
    
    // 使用 * 操作符显式(手动)解引用 Box,获取其内部值
    println!("*b = {}", *b);         // 输出: *b = 5
    
    // 解引用后可修改 Box 内部的值(需声明为 mut,并非指针后续会变化,只是为了允许解引用后修改值)
    let mut num = Box::new(10);
    *num += 5;                      // 修改堆上的值
    println!("num after modification: {}", *num);  // 输出: 15
    
    // 函数参数传递 Box 的引用
    print_value(&b);        // 传递不可变引用,不转移所有权
    
    // 直接传递 Box 会转移所有权
    print_value_direct(b);  // b 的所有权被转移到函数中,之后无法再使用 b
}

// 接受 Box 引用的函数
// 参数类型 &Box<i32> 表示指向 Box 的引用
fn print_value(num: &Box<i32>) {
    // 第一次解引用:从 &Box<i32> 变为 Box<i32>
    // 第二次解引用:从 Box<i32> 变为 i32
    println!("Value inside box: {}", **num);  // 输出: Value inside box: 5
}

// 接受 Box 所有权的函数
// 参数类型 Box<i32> 会转移 Box 的所有权
fn print_value_direct(num: Box<i32>) {
    // 只需一次解引用即可获取内部值
    println!("Value inside box: {}", *num);   // 输出: Value inside box: 5
    // num 在此处被丢弃,堆内存被释放
}

Box<T> 常用于实现链表结构

rust
// 用于定义递归类型:链表
enum List {
    Cons(i32, Box<List>),// 因为指针的大小是确定的,rust可以推断存储所需的内存大小。
					// 若直接存放值,而非指针的话 rust 判定该链表的大小是无限,拒绝编译。
    Nil,
}
// 以下链表结构会被拒绝编译
// enum List {
//     Cons(i32, List),
//     Nil,
// }

use List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Nil))));
    // Box 让递归类型有确定大小
    if let Cons(head, tail) = list {
        println!("Head = {}", head);
    }
}

自定义智能指针

Deref Trait

之前讲过 Trait 是一种约束,但对于第三方库而言,标准库里的 Trait 其实更像是一种公约。

只要你自己写的智能指针实现了标准库的 Deref Trait,那么就可以用解引用运算符(没错就是*)对你的指针解引用,从而拿到值,或对值进行修改。

写一个自己的智能指针

rust
struct MyBox<T>(T);	// 元组结构体,它包含一个泛型类型T的字段
					// 嚯嚯嚯,智能指针就是一个结构体+结构体类型的实现

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);	// 这一句无法通过编译,因为你的指针还没有实现 Deref Trait
}

实现一下 Deref Trait:

rust
use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl<T> Deref for MyBox<T> {	// 这里
    type Target = T;

    fn deref(&self) -> &Self::Target {	// 返回一个引用
        &self.0	// 元组的第一个数据(也是唯一一个),也就是泛型T
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);	// *y 实际调用的代码是 *(y.deref())
}

实现这一 Trait 的重要之处并非“可以使用*解引用”,而是 rust 也可以帮你自动解引用了,尤其是在套了好几层引用的情况下你就不必自己去翻代码查看需要解引用几次。

Drop Trait

drop,之前理解为释放内存,但实际上它是 作用域结束时执行的函数块

实现了 Drop Trait 之后rust可以自动执行,但不能手动执行。若需要手动执行则需要调用标准库的drop Std::mem::drop,代码是 drop(指针变量); 而不是 Drop Trait 的 指针变量.drop();

Rc<T>

引用计数智能指针。

这是一个单线程的不可变引用(数据是只读的),但可以同时被多个所有者使用。

rust
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));	// 通过 Rc::clone(&a) 函数增加引用计数,也可以使用 a.clone()
    let c = Cons(4, Rc::clone(&a));
}

可以通过打印 Rc::stron_count(&a) 查看当前引用数:

rust
fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));	// 引用的初始值为 1
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

输出为:

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

RefCell<T>

一表讲明白内部可变性是啥。

不可变引用可变引用内部可变性模式
可以同时拥有多个所有者,但不可以改变值只能同时有一个所有者,但可以改变值因为是不可变引用所以可以拥有多个所有者,但又可以通过指针的内部方法改变值,是不是很牛逼

官方文档在这一部分提及了【mock 对象】,这玩意儿说白了就是一个测试用例,是一个写在测试模块里的伪造类型,模仿了实际使用的类型的接口但不具备实际功能,是为了在测试时能直观地看到该类型的被使用的情况(仅测试是否被调用,以及调用的参数,不测试该类型/方法的具体功能。毕竟本身就是伪造的说)。这在某些语言里是标准库自带的功能,而在 rust 可以以这种形式模仿。

RefCell<T>的使用:

rust
#[cfg(test)] // 这个属性表示下面的模块只在测试时编译
mod tests {
    use super::*; // 导入外部模块的所有内容
    use std::cell::RefCell; // 引入RefCell类型,它允许在不可变引用下修改数据

    // MockMessenger是一个模拟对象,用于测试Messenger trait的实现
    // 在测试中,我们通常不想依赖真实的外部资源(如网络、文件系统)
    // 而是创建一个简单的替代品来验证代码逻辑
    struct MockMessenger {
        // RefCell提供"内部可变性",允许我们在不可变引用(&self)下修改数据
        // 这在Rust中很特别,因为通常不可变引用不能修改数据
        // RefCell在运行时检查借用规则,而不是编译时:
        // 	-	原因:为了实现“内部可变性”,它使用了unsafe代码,所以编译器无法检查是否安全。
        //		RefCell本身使用不可变借用,但内部方法可返回可变或不可变的指针,
        //		若代码不遵守借用规则,则会在运行时恐慌(panic)
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            // 创建一个新的MockMessenger实例
            // 初始化sent_messages作为一个RefCell包装的空动态数组
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    // 为MockMessenger实现Messenger trait(让MockMessenger具备真实Messenger的实现)
    impl Messenger for MockMessenger {
        // send方法接收一个不可变的self引用(&self)
        // 正常情况下,不可变引用不能修改对象的状态
        // 但通过RefCell,我们可以修改内部数据
        fn send(&self, message: &str) {
            // borrow_mut()获取RefCell内部数据的可变引用
            // 这会在运行时检查是否有其他借用存在(类似编译时的借用检查)【重要】
            // 如果没有活跃的不可变借用,borrow_mut()将返回一个可变引用【否则会恐慌/测试失败】
            // 然后我们可以向向量中添加消息
            // 顺带一提,如果用的是 borrow(),则为不可变引用
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--

        // 断言mock对象记录了一条消息
        // borrow()获取RefCell内部数据的不可变引用
        // 同样在运行时检查借用规则(此时不能有活跃的可变借用)
        // 这里我们检查数组的长度是否为1
        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

总结一下 RefCell<T> 的使用:

RefCell<T> 是一个容器,除了存放值,还提供两个内部方法分别返回两种指针

内部方法返回的指针类型可否更改值借用规则(运行时检查)
borrow_mut()可变引用可以同一时间只能有一个存在,包括不能同时使用 borrow() 的不可变引用
borrow()不可变引用不可可以有多个不可变引用,但不能同时使用 borrow_mut() 的可变引用

PS:看注释学习是不是舒服多了。

Rc<T> 和 RefCell<T> 结合使用

上文提到,RefCell<T> 本身是唯一所有权的,只能被一个变量持有,可变引用也只能有一个。

诶,你说巧了不是,正好和 Rc<T>引用计数指针 一结合,啪的一下蹦出了好多个可变引用。

等等,这不是魔法,上文也同样提过:它使用的是运行时借用规则,所以即便代码里看着是使用了好几个borrow_mut()borrow(),他们之间也是不能同时执行的,否则会panic。换句话说,就是不能用在多线程中,就这么简单。(多线程中有类似的东西,叫Mutex<T>

引用循环导致内存泄漏

占着内存不释放,就是内存泄露。内存泄露不违反rust的规则(即不会导致悬垂指针、数据竞争等未定义的行为)。

即使发生内存泄漏,Rust 仍然保证:

  • 不会访问已释放的内存(悬垂指针)。
  • 不会出现数据竞争(线程安全)。
  • 所有借用规则仍然有效(不会同时存在可变和不可变借用冲突)。

所以它仍然是内存安全的。

引用循环是如何发生的

rust
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

// 定义一个链表枚举,可以是包含值的节点(Cons)或空节点(Nil)
#[derive(Debug)]
enum List {
    // Cons包含一个整数和一个可变的引用计数指针(RefCell<Rc<List>>)
    // RefCell允许运行时借用检查,Rc是引用计数指针
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    // 获取链表下一个节点的方法
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),  // 如果是Cons节点,返回下一个节点的引用
            Nil => None,                  // 如果是Nil节点,返回None
        }
    }
}

fn main() {
    // 创建第一个链表节点a,值为5,下一个节点是Nil
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    // 打印a的初始引用计数(应该是1)
    println!("a initial rc count = {}", Rc::strong_count(&a));
    // 打印a的下一个节点(应该是Nil)
    println!("a next item = {:?}", a.tail());

    // 创建第二个链表节点b,值为10,下一个节点指向a
    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    // 打印创建b后a的引用计数(应该是2,因为a被b引用)
    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    // 打印b的初始引用计数(应该是1)
    println!("b initial rc count = {}", Rc::strong_count(&b));
    // 打印b的下一个节点(应该是指向a的Cons(5, Nil))
    println!("b next item = {:?}", b.tail());

    // 这里开始制造引用循环
    if let Some(link) = a.tail() {
        // 修改a的下一个节点,让它指向b
        // 现在的情况:
        // b -> a -> b -> a -> ... 形成循环
        *link.borrow_mut() = Rc::clone(&b);
    }

    // 打印修改后b的引用计数(应该是2,因为a现在引用b)
    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    // 打印修改后a的引用计数(应该是2,因为b引用a)
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // 如果取消下面这行注释,程序会尝试打印循环链表
    // 由于循环无限嵌套,会导致栈溢出
    // println!("a next item = {:?}", a.tail());
}
  • Rc的引用计数永远不会归零(a和b互相引用)
  • a和b在main函数里,是全局作用域(没有任何强制释放的时机),于是内存泄漏发生了
  • 是代码逻辑问题,rust无法自动检测这种问题,应该使用自动化测试、代码审查和其他开发实践来避免。

使用弱引用 Weak<T>

记不下去了,用到的时候再说吧。