The Rust Programming Language - 第15章 智能指针 - 15.6 引用循环和内存泄漏是安全的

15 智能指针

指针指向变量的内存地址,除了引用数据没有其它的功能,因此没有运行开销

智能指针是一类数据结构,虽然表现类似指针,但是拥有额外的元数据和功能。Rust的智能指针提供了包含引用之外的其他功能,但是指针这个概念并不是Rust独有的

在Rust中,普通指针只是借用数据,而智能指针还拥有它们指向的数据,比如String和Vec,它们都是智者指针,它们拥有数据并且可以被修改。它们也带有元数据(比如容量)和额外的功能和保证(String的数据总是有效的UTF-8编码)

智能指针通常使用结构体来实现,区别与常规结构体的是它们实现了deref 和 drop trait。deref trait允许智能指针结构体实例表现的像引用一样,这样可以编写既可以用于引用又可以用于智能指针的代码。Drop trait 允许我们自定义智能指针离开作用域时运行的代码

智能指针在Rust中是一个通用设计模式,很多库都有智能指针并且你也可以编写自己的智能指针,本章我们将会学习以下:

Box,用于在堆上分配值,实现了Deref trait值,允许Box被当作引用对待,当Box离开作用域时,由于Box类型Drop trait的实现,box所指向的数据也会被清除

Rc,一个引用计数类型,其数据可以有多个所有者

Ref和RefMut,通过RefCell访问(RefCell是一个在运行时而不是在编译时执行借用规则的类型)

我们还会涉及内部可变性模式,这是不可变类型暴露出改变其内部值的API,我们也会讨论引用循环会如何泄露内存,以及如何避免

15.6 引用循环和内存泄漏是安全的

Rust的内存安全行保证较难出现内存泄漏,但是难并不代表不可能。,这意味内存泄漏在Rust被认为是安全的。这一点通过Rc和RefCell可以看得出:创建引用循环的可能性时存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了0,其值也永远不会被丢弃

制造引用循环

我们来看一个例子,看看引用循环如何发生以及如何避免

use std::{cell::Ref, 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,
        }
    }
}

这里Cons的第二个元素类型是RefCell<Rc>,这意味着我们想修改Cons成员所指向的List。这里我们还定义了一个方法来方便我们在有Cons成员的时候访问其第二项

我来写一个例子:如下代码在a中创建了一个列表,一个指向a中的列表的b列表,接着修改a中的列表指向b中的列表,这就是一个引用循环。在这个过程的多个位置有println!语句展示引用计数

use std::borrow::BorrowMut;
use std::{cell::Ref, 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))));
   
   
   println!("a initial rc count = {}",Rc::strong_count(&a));
   println!("a next item = {:?}",a.tail());

   let b = Rc::new(Cons(10,RefCell::new(Rc::clone(&a))));

   println!("a rc count after b creation  = {}",Rc::strong_count(&a));
   println!("b initial rc count = {}",Rc::strong_count(&b));
   println!("b next item = {:?}",b.tail());

   if let Some(link) = a.tail() {
       *link.borrow_mut() = Rc::clone(&b);
   }

   println!("b rc count after changing a = {}",Rc::strong_count(&b));
   println!("a rc count after changing a = {}",Rc::strong_count(&a));
}

我们运行一下来看看结果

a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation  = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

列表a和列表b彼此相互指向形成循环

这样的循环还行,但是如果是比较复杂的循环的话,程序的内存压力就会非常大

所以在使用Rc和RefCell时请务必注意没有形成引用循环,当然我们可以重新组织数据结构来尝试避免这种问题

避免引用循环:将Rc变为Weak

截至目前,我们已经知道调用 Rc::clone会增加Rc实例的strong_count,并且只有在strong_count为0时Rc才会被清理

现在我们可以通过调用Rc::downgrade 并传递Rc实例的引用来创建其值的弱引用

调用Rc::downgrade会得到weak类型的智能指针。不同于将Rc实例的strong_count加1,调用Rc::downgrade会将weak_count加1.Rc类型使用weak_count来记录其存在多少个weak引用,类似于strong_count,区别在于weak_count无需计数为0就能使Rc实例被清理

强引用代表如何共享Rc实例的所有权,但是弱引用并不属于所以权关系。它们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为0时被打断

因为Weak引用的值可能已经被丢弃了,为了使其的指向有效,我们调用weak实例的upgrade方法,这会返回Option<Rc>.如果Rc值还未被丢弃,结果是Some,如果丢弃结果是None。因为upgrade方法会返回一个Option,我们确信Rust会处理Some和None的情况,所以它不会返回非法指针

创建树形数据结构:带有子节点的Node

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

我们希望Node能够拥有其子节点,同时也希望能通过变量共享所有权,这样可以直接访问树中的每一个Node,为此Vec的项的类型被定义为Rc.我们还希望能够修改其他节点的子节点,所以children中的Vec<Rc>被放进了RefCell

fn main(){
   let leaf = Rc::new(Node{
       value:3,
       children: RefCell::new(vec![]),
   });
   let branch = Rc::new(Node {
       value: 5,
       children:RefCell::new(vec![Rc::clone(&leaf)]),
   });

创建没有子节点的Leaf节点和以leaf作为子节点的branch节点

因为克隆,所以leaf中的Node现在有两个所有这:leaf和branch。可以通过branch.children从branch中获得leaf,不过无法从leaf到branch。leaf没有到branch的引用且不知道它们相互关联。我们希望Leaf知道branch是它的父节点

增加从子到父的引用

我们知道子节点节点和父节点的关系应该是丢弃父节点,子节点应该也被丢弃,而丢弃子节点,父节点仍然应该存在,如果使用Rc的话,父引用子,子引用父,这样会导致循环引用,但是使用Weak,子节点引用父节点是弱引用,就不会导致循环引用的问题,同时也达到了我们的目的,让子节点知道了它的父节点

我们先在Node中增加一个元素parent

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

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

我们再来重构各个节点的引用

fn main(){
   let leaf = Rc::new(Node{
       value:3,
       parent:RefCell::new(Weak::new()),
       children: RefCell::new(vec![]),
   });

   println!("leaf parent is {:?}",leaf.parent.borrow().upgrade());

   let branch = Rc::new(Node {
       value: 5,
       parent:RefCell::new(Weak::new()),
       children:RefCell::new(vec![Rc::clone(&leaf)]),
   });
   *leaf.parent.borrow_mut() = Rc::downgrade(&branch);
   println!("leaf parent = {:?}",leaf.parent.borrow().upgrade())
}

运行一下结果,结果是发现leaf已经知道了它的父节点

     Running `target/debug/smartPoint`
leaf parent is None
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })

可视化strong_count和weak_count 的改变

fn main(){
   let leaf = Rc::new(Node{
       value:3,
       parent:RefCell::new(Weak::new()),
       children: RefCell::new(vec![]),
   });
   println!(
       "leaf strong = {},weak = {}",
       Rc::strong_count(&leaf),
       Rc::weak_count(&leaf),
   );
   
   let branch = Rc::new(Node {
       value: 5,
       parent:RefCell::new(Weak::new()),
       children:RefCell::new(vec![Rc::clone(&leaf)]),
   });
   *leaf.parent.borrow_mut() = Rc::downgrade(&branch);
   
   println!(
       "branch strong = {},weak ={}",
       Rc::strong_count(&branch),
       Rc::weak_count(&branch),
   );
   println!(
       "leaf strong = {},weak = {} ",
       Rc::strong_count(&leaf),
       Rc::weak_count(&leaf),
   );
}
    Running `target/debug/smartPoint`
leaf strong = 1,weak = 0
branch strong = 1,weak =1
leaf strong = 2,weak = 0 

在内部作用域创建branch并检查强弱引用计数

所有这些管理计数和值的逻辑都内建于Rc和Weak以及它们的Drop trait实现中。通过在Node定义中指定从子节点到父节点的关系为一个Weak引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏

总结:这一章我们介绍了几种不同的智能指针

Box有一个已知大小并指向分配在堆上的数据

Rc记录了堆上数据的引用数量以便可以拥有多个所有者

RefCell和其内部的可变性提供了一个可以用于当需要不可变类型但是需要改变其内部值能力的类型,并在运行时而不是编译时检查借用规则

下一章我们会了解一下Rust的并发

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《The Rust Programming Language》(Rust编程语言)是一本由Rust开发社区编写的权威指南和教程,用于学习和开发Rust编程语言。 Rust编程语言是一种开源、现代化的系统级编程语言,具有强大的内存安全性、并发性和性能。它最初由Mozilla开发,并于2010年首次发布。Rust的设计目标是实现安全、并发和快速的系统级编程,适用于像操作系统、浏览器引擎和嵌入式设备这样的低级应用程序。 《The Rust Programming Language》提供了对Rust编程语言的全面介绍。它从基本的语法和数据类型开始,然后涵盖了Rust的所有关键概念和特性,如所有权系统、借用检查器、模块化和并发编程等。这本书不仅适合初学者,还可以作为更有经验的开发者的参考手册。 书中详细介绍了Rust的主要特性,例如所有权系统,它可以避免常见的内存错误,如空指针和数据竞争。同时,该书还着重介绍了Rust的错误处理机制和泛型编程。读者将学习如何使用Rust编写高效、安全和易于维护的代码。 《The Rust Programming Language》还包含许多实用的示例代码和练习,帮助读者通过实践加深对Rust的理解。此外,书中还介绍了一系列构建工具和库,以及有用的开发工作流程。 总之,《The Rust Programming Language》为学习和开发Rust编程语言的人们提供了清晰、全面的指南。无论您是初学者还是有经验的开发者,都可以从中受益,提高Rust编程的技能和效率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值