当前位置: 首页 > article >正文

Rust从入门到精通之进阶篇:16.智能指针

智能指针

智能指针是一种数据结构,它们的行为类似于指针,但具有额外的元数据和功能。在 Rust 中,智能指针通常实现了 DerefDrop 特质,允许它们像引用一样工作并在离开作用域时自动清理资源。在本章中,我们将探索 Rust 中的各种智能指针类型及其用途。

引用回顾

在深入智能指针之前,让我们先回顾一下 Rust 中的普通引用:

fn main() {
    let x = 5;
    let y = &x;  // y 是 x 的引用
    
    assert_eq!(5, x);
    assert_eq!(5, *y);  // 使用解引用运算符 * 访问引用的值
}

引用是 Rust 中最简单的指针类型,它们没有任何特殊功能,只是借用值而不获取所有权。

Box

Box<T> 是 Rust 中最简单的智能指针类型,它允许你将值存储在堆上而不是栈上:

fn main() {
    let b = Box::new(5);  // 在堆上分配值 5
    println!("b = {}", b);
    
    // 可以像使用引用一样使用 Box
    assert_eq!(5, *b);
} // b 离开作用域时,它指向的堆内存会被自动释放

Box 的主要用途

1. 存储已知大小但较大的数据

当你有一个较大的数据结构,但不想在栈上分配内存时,可以使用 Box

struct LargeStruct {
    data: [u8; 1000000],  // 1MB 的数据
}

fn main() {
    // 在栈上分配可能导致栈溢出
    // let large_struct = LargeStruct { data: [0; 1000000] };
    
    // 在堆上分配更安全
    let large_struct = Box::new(LargeStruct { data: [0; 1000000] });
    
    println!("结构体已创建");
}
2. 创建递归类型

递归类型的大小在编译时无法确定,因此需要使用 Box 来创建:

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

fn main() {
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));
    
    // 使用模式匹配访问列表元素
    let mut current = &list;
    while let List::Cons(value, next) = current {
        println!("值: {}", value);
        current = next;
    }
}
3. 特质对象

Box 可以用来创建特质对象,允许在运行时使用动态分发:

trait Draw {
    fn draw(&self);
}

struct Circle {
    radius: f64,
}

impl Draw for Circle {
    fn draw(&self) {
        println!("画一个半径为 {} 的圆", self.radius);
    }
}

struct Square {
    side: f64,
}

impl Draw for Square {
    fn draw(&self) {
        println!("画一个边长为 {} 的正方形", self.side);
    }
}

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

Deref 特质

Deref 特质允许自定义解引用运算符 * 的行为。智能指针通过实现 Deref 特质,使它们的行为类似于引用:

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
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);
    
    assert_eq!(5, x);
    assert_eq!(5, *y);  // 相当于 *(y.deref())
}

解引用强制转换

Rust 提供了解引用强制转换(deref coercion)功能,当将一个实现了 Deref 特质的类型的值作为参数传递给函数或方法时,如果参数类型不匹配,Rust 会自动应用 deref 方法:

fn hello(name: &str) {
    println!("你好,{}!", name);
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);  // 解引用强制转换:&MyBox<String> -> &String -> &str
}

Drop 特质

Drop 特质允许你自定义当值离开作用域时发生的行为。这对于释放资源(如文件句柄或网络连接)非常有用:

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("释放 CustomSmartPointer,数据: `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer { data: String::from("我的数据") };
    let d = CustomSmartPointer { data: String::from("其他数据") };
    println!("创建了智能指针");
    // c 和 d 在这里离开作用域,Rust 会自动调用它们的 drop 方法
}

提前丢弃值

有时你可能需要提前丢弃一个值。Rust 提供了 std::mem::drop 函数来实现这一点:

fn main() {
    let c = CustomSmartPointer { data: String::from("提前丢弃") };
    println!("创建了智能指针");
    drop(c);  // 手动调用 drop 函数
    println!("在 main 函数结束前丢弃了智能指针");
}

Rc

Rc<T>(引用计数,Reference Counting)允许多个所有者共享同一数据的所有权。当最后一个所有者离开作用域时,数据才会被清理:

use std::rc::Rc;

fn main() {
    let a = Rc::new(5);  // 创建一个引用计数的值
    println!("创建 a,引用计数 = {}", Rc::strong_count(&a));  // 1
    
    let b = Rc::clone(&a);  // 增加引用计数,而不是复制数据
    println!("创建 b,引用计数 = {}", Rc::strong_count(&a));  // 2
    
    {
        let c = Rc::clone(&a);  // 再次增加引用计数
        println!("创建 c,引用计数 = {}", Rc::strong_count(&a));  // 3
    }  // c 离开作用域,引用计数减少
    
    println!("c 离开作用域后,引用计数 = {}", Rc::strong_count(&a));  // 2
    
    // 可以通过任何一个引用访问数据
    println!("a = {}, b = {}", a, b);
}  // a 和 b 离开作用域,引用计数变为 0,数据被清理

Rc 的限制

Rc<T> 只能用于单线程场景,并且只提供不可变访问:

use std::rc::Rc;

fn main() {
    let a = Rc::new(vec![1, 2, 3]);
    let b = Rc::clone(&a);
    
    // 错误:不能获取可变引用
    // a.push(4);
    
    println!("a = {:?}, b = {:?}", a, b);
}

RefCell 和内部可变性

内部可变性是 Rust 的一种设计模式,它允许你在拥有不可变引用的情况下修改数据。RefCell<T> 提供了内部可变性:

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);
    
    // 获取不可变引用
    let a = data.borrow();
    println!("a = {}", a);
    
    // 必须先释放不可变引用,才能获取可变引用
    drop(a);
    
    // 获取可变引用并修改值
    let mut b = data.borrow_mut();
    *b += 1;
    println!("b = {}", b);
}

借用规则在运行时检查

与普通引用不同,RefCell<T> 在运行时而不是编译时检查借用规则:

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);
    
    let a = data.borrow();
    let b = data.borrow();  // 可以同时有多个不可变借用
    
    println!("a = {}, b = {}", a, b);
    
    // 下面的代码会在运行时 panic,因为已经有不可变借用
    // let mut c = data.borrow_mut();
}

结合 Rc 和 RefCell

Rc<T>RefCell<T> 经常一起使用,以提供多所有者和内部可变性:

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

fn main() {
    let data = Rc::new(RefCell::new(vec![1, 2, 3]));
    
    let a = Rc::clone(&data);
    let b = Rc::clone(&data);
    
    // 通过 a 修改数据
    a.borrow_mut().push(4);
    
    // 通过 b 也能看到修改后的数据
    println!("b = {:?}", b.borrow());
}

Weak

Weak<T> 提供了对 Rc<T> 数据的非所有权引用,不会增加强引用计数,因此不会阻止数据的清理:

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

#[derive(Debug)]
struct Node {
    value: i32,
    parent: Option<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: None,
        children: RefCell::new(vec![]),
    });
    
    println!("leaf 强引用计数 = {}", Rc::strong_count(&leaf));  // 1
    println!("leaf 弱引用计数 = {}", Rc::weak_count(&leaf));    // 0
    
    {
        let branch = Rc::new(Node {
            value: 5,
            parent: None,
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });
        
        // 设置 leaf 的父节点为 branch,使用 Weak 引用避免循环引用
        leaf.parent = Some(Rc::downgrade(&branch));
        
        println!("branch 强引用计数 = {}", Rc::strong_count(&branch));  // 1
        println!("branch 弱引用计数 = {}", Rc::weak_count(&branch));    // 1
        
        println!("leaf 强引用计数 = {}", Rc::strong_count(&leaf));      // 2
        println!("leaf 弱引用计数 = {}", Rc::weak_count(&leaf));        // 0
        
        // 访问 leaf 的父节点
        if let Some(parent) = &leaf.parent {
            // 尝试将 Weak 引用升级为 Rc
            if let Some(parent) = parent.upgrade() {
                println!("leaf 的父节点是 {}", parent.value);
            }
        }
    }  // branch 离开作用域,强引用计数变为 0,数据被清理
    
    // branch 已被清理,所以 leaf.parent 现在是悬空的 Weak 引用
    println!("leaf 强引用计数 = {}", Rc::strong_count(&leaf));  // 1
    
    // 尝试访问已清理的父节点
    if let Some(parent) = &leaf.parent {
        if let Some(_) = parent.upgrade() {
            println!("leaf 的父节点仍然存在");
        } else {
            println!("leaf 的父节点已被清理");
        }
    }
}

Arc

Arc<T>(原子引用计数,Atomic Reference Counting)是 Rc<T> 的线程安全版本,可以在多线程环境中安全地共享数据:

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

fn main() {
    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();
    }
}

Mutex

Mutex<T>(互斥锁)提供了线程安全的内部可变性,确保在任何时刻只有一个线程可以访问数据:

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];
    
    for _ in 0..10 {
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("结果: {}", *counter.lock().unwrap());
}

上面的代码会编译失败,因为 counter 的所有权在第一个线程中被移动。要解决这个问题,我们需要结合 ArcMutex

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("结果: {}", *counter.lock().unwrap());
}

RwLock

RwLock<T>(读写锁)允许多个读取器或一个写入器访问数据:

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

fn main() {
    let data = Arc::new(RwLock::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 data = data_clone.read().unwrap();
            println!("读取线程 {}: 数据 = {:?}", i, *data);
        });
        handles.push(handle);
    }
    
    // 创建一个写入线程
    let data_clone = Arc::clone(&data);
    let handle = thread::spawn(move || {
        let mut data = data_clone.write().unwrap();
        data.push(4);
        println!("写入线程: 数据 = {:?}", *data);
    });
    handles.push(handle);
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("最终数据: {:?}", *data.read().unwrap());
}

智能指针的选择

以下是选择合适智能指针的指南:

智能指针所有权可变性线程安全主要用途
Box<T>单一所有者可变或不可变取决于 T堆分配、递归类型、特质对象
Rc<T>多所有者不可变单线程共享所有权
RefCell<T>单一所有者内部可变性单线程内部可变性
Arc<T>多所有者不可变多线程共享所有权
Mutex<T>单一所有者内部可变性多线程互斥访问
RwLock<T>单一所有者内部可变性多线程读写访问

自定义智能指针

你可以通过实现 DerefDrop 特质来创建自己的智能指针:

use std::ops::{Deref, DerefMut};
use std::fmt;

struct SmartPointer<T> {
    value: T,
    name: String,
}

impl<T> SmartPointer<T> {
    fn new(value: T, name: &str) -> SmartPointer<T> {
        println!("创建智能指针 {}", name);
        SmartPointer {
            value,
            name: name.to_string(),
        }
    }
}

impl<T> Deref for SmartPointer<T> {
    type Target = T;
    
    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

impl<T> DerefMut for SmartPointer<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.value
    }
}

impl<T> Drop for SmartPointer<T> {
    fn drop(&mut self) {
        println!("释放智能指针 {}", self.name);
    }
}

impl<T: fmt::Display> fmt::Display for SmartPointer<T> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "SmartPointer({}: {})", self.name, self.value)
    }
}

fn main() {
    let mut a = SmartPointer::new(5, "a");
    let b = SmartPointer::new(10, "b");
    
    println!("a = {}, b = {}", a, b);
    println!("*a = {}, *b = {}", *a, *b);
    
    *a += 1;  // 使用 DerefMut
    println!("修改后: a = {}", a);
}

最佳实践

1. 选择合适的智能指针

根据你的需求选择合适的智能指针:

  • 需要在堆上分配数据?使用 Box<T>
  • 需要共享所有权?使用 Rc<T>Arc<T>
  • 需要内部可变性?使用 RefCell<T>Mutex<T>/RwLock<T>

2. 避免循环引用

使用 Rc<T>Arc<T> 时,要小心避免循环引用,这可能导致内存泄漏:

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

#[derive(Debug)]
struct Node {
    value: i32,
    // 使用 Weak<T> 而不是 Rc<T> 避免循环引用
    // children: RefCell<Vec<Rc<Node>>>,
    // parent: Option<Rc<Node>>,
}

fn main() {
    // 这会导致循环引用和内存泄漏
    let a = Rc::new(RefCell::new(Node { value: 5 }));
    let b = Rc::new(RefCell::new(Node { value: 10 }));
    
    // a 引用 b
    a.borrow_mut().children.push(Rc::clone(&b));
    // b 引用 a
    b.borrow_mut().parent = Some(Rc::clone(&a));
}

3. 使用 clone 而不是引用

使用 Rc::cloneArc::clone 而不是引用,以明确表示你的意图:

use std::rc::Rc;

fn process(data: Rc<Vec<i32>>) {
    println!("处理数据: {:?}", *data);
}

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    
    // 好的做法:使用 clone 明确表示共享所有权
    process(Rc::clone(&data));
    
    // 仍然可以使用原始数据
    println!("原始数据: {:?}", *data);
}

4. 尽量减少锁的作用域

使用 MutexRwLock 时,尽量减少锁的作用域,以避免阻塞其他线程:

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

fn main() {
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));
    let data_clone = Arc::clone(&data);
    
    let handle = thread::spawn(move || {
        // 不好的做法:锁的作用域太大
        let mut data = data_clone.lock().unwrap();
        // 执行耗时操作...
        thread::sleep(std::time::Duration::from_secs(1));
        data.push(4);
    });
    
    // 好的做法:减小锁的作用域
    {
        let mut data = data.lock().unwrap();
        data.push(5);
    }  // 锁在这里被释放
    
    // 执行其他操作...
    
    handle.join().unwrap();
    println!("最终数据: {:?}", *data.lock().unwrap());
}

5. 使用 parking_lot

考虑使用 parking_lot 库,它提供了更高性能的互斥原语:

// Cargo.toml
// [dependencies]
// parking_lot = "0.12.0"

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

fn main() {
    let data = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            // 不需要 unwrap,锁定失败会 panic
            let mut num = data_clone.lock();
            *num += 1;
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("结果: {}", *data.lock());
}

练习题

  1. 实现一个简单的二叉树数据结构,使用 Box<T> 存储节点。实现插入、搜索和遍历操作。

  2. 创建一个简单的对象池,使用 Rc<T>RefCell<T> 管理可重用对象。实现获取和释放对象的方法。

  3. 实现一个线程安全的计数器,使用 Arc<T>Mutex<T>。创建多个线程增加计数器的值,并验证最终结果。

  4. 创建一个简单的观察者模式实现,其中主题使用 Weak<T> 引用存储观察者,以避免循环引用。

  5. 实现一个自定义智能指针,它可以记录解引用操作的次数。实现 DerefDerefMutDrop 特质,并在程序结束时打印统计信息。

总结

在本章中,我们探讨了 Rust 中的智能指针:

  • Box<T> 用于在堆上分配数据
  • Rc<T> 用于单线程环境中的共享所有权
  • RefCell<T> 用于单线程环境中的内部可变性
  • Arc<T> 用于多线程环境中的共享所有权
  • Mutex<T>RwLock<T> 用于多线程环境中的内部可变性
  • Weak<T> 用于避免循环引用

我们还学


http://www.kler.cn/a/611440.html

相关文章:

  • UML 图六种箭头含义详解:泛化、实现、依赖、关联、聚合、组合
  • LeetCode热题100JS(79/100)第十五天|347|295|121|55|45
  • 初级:反射机制面试题全攻略
  • Vue Router动态改变路由参数的两种方法
  • Rust从入门到精通之进阶篇:18.测试与文档
  • 淘宝评论API接口详解与JSON数据示例
  • Unity Shader编程】之复杂光照
  • Java技术生态前沿:Java 21革新与性能优化全解析
  • leetcode 46 全排列 | 回溯
  • 重学vue3(三):vue3基本语法及使用
  • 【测试开发】OKR 小程序端黑盒测试报告
  • leetcode.189.轮转数组
  • ZBlog泛目录程序插件实现零编程基础实现自动化内容生成
  • 一、MySQL8的my.ini文件
  • 【Python】pillow库学习笔记4-利用ImageDraw和ImageFont在图像上添加文字
  • sqlite3数据库(文件)损坏恢复方法
  • 【论文分析】无人机轨迹规划,Fast-Planner:实时避障+全局最优的路径引导优化算法
  • 护网(蓝中)DNS面试题
  • 【蓝桥杯】真题 路径(数论+dp)
  • MATLAB 编写的函数或算法生成可供 C++ 调用的库或组件