《Rust权威指南》 第15章 智能指针

之前我们一直使用的引用类型,就其本质而言,实际上就是指针(正如我们大家都 “爱” 的C++那样,Rust中的指针也是一段地址)。而本节将要讨论的智能指针,更是与C++中的智能指针(unique_ptr<T>,shared_ptr<T>等)有太多相似之处
但是先别急眼,大体上我们不用在使用Rust时进行对于变量的类型的沉思(不要尝试理解&RefCell<Rc<String>>到底是什么东西,是不是指针的指针的指针),而是根据功能来进行思考
比如,正常的引用&,一般就是用在函数传参和各种切片类型中,我们在使用时不需要思考其“指针”的本质,只是知道它遵循“借用”的规则即可

Deref trait 和 Drop trait

基本上智能指针就是通过为类型实现这两个trait实现的
有很多类型都是“类似智能指针”,比如String类型等

通过Deref trait将智能指针视作常规指针(引用)

实现Deref trait让我们可以自由定义解引用算符*的行为

该trait要求我们实现一个deref方法,该方法借用self并返回一个指向内部数据的引用

struct MyBox<T>(T);

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

use std::ops::Deref;
impl<T> Deref for MyBox<T> {
	type Target = T;

	fn deref(&self) -> &T {
		&self.0
	}
}

实现该trat后,所有的*mybox都会被隐式的展开为*(mybox.deref())

函数和方法的隐式解引用转换

当传入类型和参数类型不一样时,编译器就会尝试隐式的调用deref,将T的引用转换为T经过.deref()操作后生成的引用

比如一个函数接受&str为参数,你却可以传入一个&MyBox<String>。这里面分了两步

  • 调用MyBox的deref将&MyBox<String>转化到&String
  • 调用String的deref将&String转化到&str

该类行为在编译期间进行,故不产生额外的运行时开销

解引用转换与可变性

  • T:Deref<Target=U>时,允许将&T转换为&U
  • T: DerefMut<Target=U>时,允许&mut T转换为&mut U
  • T: Deref<Target=U>时,允许&mut T转换为&U

借助Drop trait在清理时运行代码

通过实现Drop trai来指定离开作用域时要运行的代码
Drop trait要求实现一个接受self可变引用作为参数的drop方法
就像是C++中为类实现析构一样的

impl<T> Drop for MyBox<T> {
	fn drop(&mut self) {
		println!("deleting...");
	}
}

使用std::mem::drop提前丢弃值

drop方法不允许被显式的调用(像析构函数一样),但是可以使用std::mem::drop来提前丢弃值(即在值离开自己的作用域之前丢弃)并调用drop方法
并不需要显式的引入std::mem::drop,只需要

drop(a);

使用Box<T>在堆上分配数据

Box在堆上为T类型分配一个空间,并取得其地址作为Box<T>的值

创建

很简单,使用关联函数new

let b = Box::new(5);

在C++中,其实就是

int *b = new int(5);

使用装箱定义递归类型

我们早就见过很多递归类型了,比如广义表,比如树
在C++中,我们是通过指针实现的
在Rust中,我们现在有智能指针后,也能达到相同的效果。
比如一个 cons list

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

let list = Cons(1,
				Box::new(Cons(2,
				Nox::new(Nil))));

我们使用C++的话可能看起来就是

struct List {
	int head;
	List* tail
}

基于引用计数的智能指针Rc<T>

Rc<T>类型的实例会在内部维护一个用于记录值引用次数的计数器,当一个值的引用次数归零,意味着该值能够被安全的清理。(类似于C++中的shared_ptr<T>
该类型被用于单个值被多个所有者所持有的情况,比如说图的数据结构

克隆Rc<T>会增加引用计数

调用Rc::clone()可以克隆克隆Rc<T>的变量,该函数接受一个&Rc<T>的参数。
该函数返回的Rc<T>和被克隆的Rc<T>指向的内存是同一个,这就实现了一个内存被多个持有者拥有

let a = Rc::new(1);
let b = Rc::clone(&a);
  • 每当调用Rc::clone(),会增加这个引用的“强引用计数”,该计数的数量会增加
  • 每当有一个引用离开生命周期,该计数会减少1
  • 当强引用计数减少为0时,释放内存
  • 可以调用Rc::strong_count(&a)来查看目前的强引用计数

RefCell<T>和内部可变性模式

内部可变性模式允许你在只持有不可变引用的前提下对数据进行更改
适用场景是什么呢,一个值能在对外部保持不变性的同时在该值的方法内部修改自身

不同于正常借用规则在编译期间检查,RefCell<T>对于借用规则的检查却是在运行时,也就是说各管各的

  • 所谓运行时检查,也就是在程序运行时的每一个单独的时刻,必须满足
  • 运行时如果检查到RefCell<T>违反了借用规则,则会产生panic

使用RefCell<T>时也与使用一般的引用有所区别

  • 使用方法.borrow()获得RefCell<T>存储的值的不可变引用
  • 使用方法.borrow_mut()获得RefCell<T>存储的值的可变引用

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

常常使用类型Rc<RefCell<T>>,定义出拥有多个所有者且能够进行修改的值

例子:一个图的数据结构

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

// 定义图节点
struct Node<T> {
    data: T,
    neighbors: Vec<Rc<RefCell<Node<T>>>>,
}

impl<T> Node<T> {
    fn new(data: T) -> Rc<RefCell<Node<T>>> {
        Rc::new(RefCell::new(Node {
            data,
            neighbors: Vec::new(),
        }))
    }

    // 添加邻居节点
    fn add_neighbor(&mut self, neighbor: Rc<RefCell<Node<T>>>) {
        self.neighbors.push(neighbor);
    }
}

// 图结构
struct Graph<T> {
    nodes: Vec<Rc<RefCell<Node<T>>>>,
}

impl<T> Graph<T> {
    fn new() -> Self {
        Graph { nodes: Vec::new() }
    }

    // 添加节点
    fn add_node(&mut self, node: Rc<RefCell<Node<T>>>) {
        self.nodes.push(node);
    }
}

fn main() {
    let mut graph = Graph::new();

    // 创建节点
    let node1 = Node::new("Node 1");
    let node2 = Node::new("Node 2");
    let node3 = Node::new("Node 3");

    // 添加邻居关系
    node1.borrow_mut().add_neighbor(Rc::clone(&node2));
    node2.borrow_mut().add_neighbor(Rc::clone(&node3));

    // 添加节点到图中
    graph.add_node(node1);
    graph.add_node(node2);
    graph.add_node(node3);

    // 输出每个节点的数据和邻居节点数量
    for node in &graph.nodes {
        let borrowed = node.borrow();
        println!("Node data: {}", borrowed.data);
        println!("Number of neighbors: {}", borrowed.neighbors.len());
        println!();
    }
}

由于循环引用早成的内存泄漏

这是一种特殊情况

创建循环引用

fn main() {}
use std::rc::Rc;
use std::cell::RefCell;
use crate::List::{Cons, Nil};

#[derive(Debug)]
enum List {
	Cons(i32, RefCell<Rc<List>>),Nil,
}

impl List {
	fn tail(&self) -> Option<&RefCell<Rc<List>>> {
		match self {
			Cons(_, item) => Some(item),
			Nil => None,
		}
	}
}
fn main() {
	let a = Rc::new(Cons(5,RefCell:new(Rc::new(Nil)))); //a:1,b:0
	let b = Rc::new(Cons(5,RefCell:new(Rc::clone(&a)))); //a:2,b:1
	if let Some(link) = a.tail() {
		*link.borrow_mut() = Rc::clone(&b); //a:2,b:2
	}
}

此时会发生什么情况呢,程序的最后,b,a先后离开作用域,强引用计数两者都减为1,但是并不为0 。所以堆上的资源不会被释放,这就导致了内存泄漏

使用Weak<T>来避免循环引用

  • 使用&Rc<T>作为参数调用Rc::downgrade(),获得Weak<T>指针
    • 当这么做时,会让调用者Rc<T>的weak_count加1
  • 使用Weak<T>的实例调用其.upgrade()方法会获得一个Option<Rc<T>>
    • Rc<T>值存在时会是Some,如果原本Rc<T>存储的值已经被释放了,就会返回None

因为资源清理只与strong_count有关,所以就算是weak_count未清零,只要strong_count已清零,内存资源就会被释放

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值