Rust能力养成系列之(42):内存管理:引用计数智能指针(下) 火星小海马

本文主要是介绍Rust能力养成系列之(42):内存管理:引用计数智能指针(下) 火星小海马,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

接着上篇的内容,我们继续

 

Rc<T>

有所忘怀的读者,可以翻出上一篇再看一下。上篇提及要修改一种结构。这里有一种做法:那就是可以把Rc<T>利用downgrade方法降级到Weak<T>类型,后者也可以通过upgrade方法升级到前者类型。downgrade的方法总是有效的。但当在Weak<T>上调用upgrade时,实际值可能已经被删除,在这种情况下,将得到一个None。

这时在上篇末尾的代码中添加一个弱指针

// rc_weak.rsuse std::rc::Rc; 
use std::rc::Weak; #[derive(Debug)] 
struct LinkedList<T> { head: Option<Rc<LinkedListNode<T>>> 
} #[derive(Debug)] 
struct LinkedListNode<T> { next: Option<Rc<LinkedListNode<T>>>, prev: Option<Weak<LinkedListNode<T>>>, data: T 
} impl<T> LinkedList<T> { fn new() -> Self { LinkedList { head: None } } fn append(&mut self, data: T) -> Self { let new_node = Rc::new(LinkedListNode { data: data, next: self.head.clone(), prev: None }); match self.head.clone() { Some(node) => { node.prev = Some(Rc::downgrade(&new_node)); }, None => { } } LinkedList { head: Some(new_node) } } 
} fn main() { let list_of_nums = LinkedList::new().append(1).append(2).append(3); println!("nums: {:?}", list_of_nums); 
}

append方法增加了一些内容;现在,在返回新创建的head之前,我们需要更新当前head的前一个节点。看起来不错了,但还不够。编译器认为这是无效操作。

当然,可以让append接收一个对self的可变引用,但这意味着只有当所有节点的绑定都可变时,才能向列表追加,从而迫使整个结构成为可变的。而此处真正想要的是一种方法,使整个结构的一小部分是可变的,幸运的是,可以一个名为RefCell的方法。

于是改善步骤如下:

1.载入RefCell

use std::cell::RefCell; 

2 将RefCell中封装LinkedListNode

 #[derive(Debug)] struct LinkedListNode<T> { next: Option<Rc<LinkedListNode<T>>>, prev: RefCell<Option<Weak<LinkedListNode<T>>>>, data: T }

3 利用改变append方法来创建一个新的RefCell,并通过RefCell可变借来更新之前的引用:

fn append(&mut self, data: T) -> Self { let new_node = Rc::new(LinkedListNode { data: data, next: self.head.Clone(), prev: RefCell::new(None) }); match self.head.Clone() { Some(node) => { let mut prev = node.prev.borrow_mut(); *prev = Some(Rc::downgrade(&new_node)); }, None => { } } LinkedList { head: Some(new_node) } } 
} 

当使用RefCell borrows(借用)时,最好仔细考虑一下我们是否在以一种安全的方式在进行使用,因为在这方面的错误可能会导致运行时崩溃。然而,在这个实现中,很容易看到我们只有一个borrow,关闭块会立即进行丢弃行为。

此时代码可以编译通过。

 

内部可变性(Interior mutability)

如前所述,Rust通过在任何给定作用域只允许一个可变引用,用来保护开发者在编译时免受指针混叠问题(pointer aliasing problem )的困扰。然而,在某些情况下,会表现得过于严格,使得明知是安全的代码,由于严格的借用检查,不能通过编译器。对于这些情况,一种解决方案是将借用检查从编译时转移到运行时,这是通过内部可变性实现的。那么在讨论允许内部可变性的类型之前,我们需要理解内部可变性(interior mutability)和继承的可变性 ( inherited mutability)的概念:

  • 继承的可变性 ( inherited mutability),当使用&mut引用某个结构体时,体现为默认的可变性,这也意味着可以修改该结构的任何字段。
  • 内部可变性(interior mutability),在这种可变性中,即使有一个&SomeStruct引用到某种类型,如果字段的类型为Cell<T>或RefCell<T>,开发者也可以修改其字段。

 

内部的可变性允许稍微变通一下借用规则,但这也给开发者带来了负担,因为要确保在运行时不存在两个可变借用。当这些类型将对多个可变引用的检测从编译转移到运行时,如果存在对某个值的两个可变引用,就会出现panic报错。当向用户公开不可变API时,通常使用内部可变性,尽管API内部有可变部分。标准库有两种提供共享可变性的通用智能指针类型:Cell和RefCell。

 

Cell<T>

考虑一下这个代码,要求用两个对bag的可变引用来改变bag:

// without_cell.rsuse std::cell::Cell; #[derive(Debug)]
struct Bag { item: Box<u32>
} fn main() { let mut bag = Cell::new(Bag { item: Box::new(1) }); let hand1 = &mut bag;let hand2 = &mut bag;*hand1 = Cell::new(Bag {item: Box::new(2)});*hand2 = Cell::new(Bag {item: Box::new(2)});
}

当然,这是通不过去的

我们可以通过将bag值封装在一个Cell中来实现这一点,代码更新如下:

// cell.rsuse std::cell::Cell; #[derive(Debug)]
struct Bag { item: Box<u32>
} fn main() { let bag = Cell::new(Bag { item: Box::new(1) }); let hand1 = &bag;let hand2 = &bag;hand1.set(Bag { item: Box::new(2)}); hand2.set(Bag { item: Box::new(3)});
}

正如所期望的那样,这个代码可以通过,唯一增加的成本是必须多写一点。但是,额外的运行时成本为零,而且对可变对象的引用仍然是不可变的,还不错吧。

Cell<T>类型是一种智能指针类型,为变量值提供可变性,即使是在不可变引用之后,就效能而言,其最小的开销和最简易的API实现了这个功能:

  • Cell::new:该方法用于通过传递任意类型T来创建Cell类型的新实例
  • get:用该方法可以复制单元格中的值,但仅在被封装的类型T为复制类型时可用
  • set:修改内部值,即使是在不可变引用后面

 

RefCell<T>

如果需要为非复制类型提供类似单元格的特征(Cell-like features),那么可以使用RefCell类型。它使用了类似于借用的读/写模式,但将检查移动到运行时,这很方便,但不是零成本。RefCell分发对值的引用,而不是像Cell类型那样按值返回。看下如下代码:

// refcell_basics.rsuse std::cell::RefCell; #[derive(Debug)]
struct Bag { item: Box<u32>
} fn main() { let bag = RefCell::new(Bag { item: Box::new(1) }); let hand1 = &bag;let hand2 = &bag;*hand1.borrow_mut() = Bag { item: Box::new(2)}; *hand2.borrow_mut() = Bag { item: Box::new(3)};let borrowed = hand1.borrow();println!("{:?}", borrowed);
}

如上所见,可以从hand1和hand2中可变借用bag,尽管它们被声明为不可变变量。要修改bag中的项,我们在hand1和hand2上调用borrow_mut。之后,进行不可改变借用并打印内容。可以通过。

RefCell类型提供了以下两种借用方法:

  • borrow方法接收一个新的不可变引用
  • borrow_mut方法接收一个新的可变引用

 

现在,如果我们尝试在同一个作用域中调用这两个方法,将前面代码的最后一行更改为,结果如下,得到一个运行时的panic。

println!("{:?} {:?}", hand1.borrow(), hand1.borrow_mut());

这是因为具备排他性可变访问的所有权规则相同。但是,对于RefCell,这是在运行时检查的。对于这种情况,必须显式使用bare block来分隔借用,或者使用drop方法来删除引用。

 

内部可变性的用法

在前一节中,我们简化了使用Cell和RefCell的用法,开发者很可能不需要在实际代码中以这种形式进行应用。这里看看这些类型给带来的一些实际益处。

正如我们前面提到的,绑定的可变性不是细粒度的;一个值要么是不可变的,要么是可变的,如果它是一个结构体或枚举,那么包括所有字段。Cell和RefCell可以把一个不可变的东西变成可变的东西,也允许将一个不可变的结构的部分定义为可变的。

下面的代码用两个整数和一个sum方法扩展了一个结构体,以缓存sum的答案,并返回缓存的值(如果存在的话):

// cell_cache.rsuse std::cell::Cell; struct Point { x: u8, y: u8, cached_sum: Cell<Option<u8>> 
} impl Point { fn sum(&self) -> u8 { match self.cached_sum.get() { Some(sum) => { println!("Got from cache: {}", sum); sum }, None => { let new_sum = self.x + self.y; self.cached_sum.set(Some(new_sum)); println!("Set cache: {}", new_sum); new_sum } } } 
} fn main() { let p = Point { x: 8, y: 9, cached_sum: Cell::new(None) }; println!("Summed result: {}", p.sum()); println!("Summed result: {}", p.sum()); 
}

编译通过,结果如下,请读者对照结果再自习研读一下代码

 

结语

Rust采用低级系统编程(low-level systems programming)方法来进行内存管理,承诺可以达到类似C语言的性能,有时甚至更好。通过使用所有权、生存期和借用语义,它不需要垃圾收集器就能做到这一点,想来还是挺赞的。至此,已经讲了很多内容,在编程学习中,都是相对较难的内容。凡是熟悉Rust的人喜欢称自己为Rustacean,期待大家能成为资深的Rustacean。有关在运行时所有权管理的内容,真的需要花一些时间,来好好思考一下,这意味着可以开发性价比很高的程序,自然这是很值得的

 

主要参考和建议读者进一步阅读的文献

https://doc.rust-lang.org/book

深入浅出 Rust,2018,范长春

Rust编程之道,2019, 张汉东

The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger

Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger

Beginning Rust ,2018,Carlo Milanesi

Rust Cookbook,2017,Vigneshwer Dhinakaran

这篇关于Rust能力养成系列之(42):内存管理:引用计数智能指针(下) 火星小海马的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/396093

相关文章

Java循环创建对象内存溢出的解决方法

《Java循环创建对象内存溢出的解决方法》在Java中,如果在循环中不当地创建大量对象而不及时释放内存,很容易导致内存溢出(OutOfMemoryError),所以本文给大家介绍了Java循环创建对象... 目录问题1. 解决方案2. 示例代码2.1 原始版本(可能导致内存溢出)2.2 修改后的版本问题在

浅析Rust多线程中如何安全的使用变量

《浅析Rust多线程中如何安全的使用变量》这篇文章主要为大家详细介绍了Rust如何在线程的闭包中安全的使用变量,包括共享变量和修改变量,文中的示例代码讲解详细,有需要的小伙伴可以参考下... 目录1. 向线程传递变量2. 多线程共享变量引用3. 多线程中修改变量4. 总结在Rust语言中,一个既引人入胜又可

大数据小内存排序问题如何巧妙解决

《大数据小内存排序问题如何巧妙解决》文章介绍了大数据小内存排序的三种方法:数据库排序、分治法和位图法,数据库排序简单但速度慢,对设备要求高;分治法高效但实现复杂;位图法可读性差,但存储空间受限... 目录三种方法:方法概要数据库排序(http://www.chinasem.cn对数据库设备要求较高)分治法(常

SpringBoot项目中Maven剔除无用Jar引用的最佳实践

《SpringBoot项目中Maven剔除无用Jar引用的最佳实践》在SpringBoot项目开发中,Maven是最常用的构建工具之一,通过Maven,我们可以轻松地管理项目所需的依赖,而,... 目录1、引言2、Maven 依赖管理的基础概念2.1 什么是 Maven 依赖2.2 Maven 的依赖传递机

Redis多种内存淘汰策略及配置技巧分享

《Redis多种内存淘汰策略及配置技巧分享》本文介绍了Redis内存满时的淘汰机制,包括内存淘汰机制的概念,Redis提供的8种淘汰策略(如noeviction、volatile-lru等)及其适用场... 目录前言一、什么是 Redis 的内存淘汰机制?二、Redis 内存淘汰策略1. pythonnoe

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

Rust 数据类型详解

《Rust数据类型详解》本文介绍了Rust编程语言中的标量类型和复合类型,标量类型包括整数、浮点数、布尔和字符,而复合类型则包括元组和数组,标量类型用于表示单个值,具有不同的表示和范围,本文介绍的非... 目录一、标量类型(Scalar Types)1. 整数类型(Integer Types)1.1 整数字

高效管理你的Linux系统: Debian操作系统常用命令指南

《高效管理你的Linux系统:Debian操作系统常用命令指南》在Debian操作系统中,了解和掌握常用命令对于提高工作效率和系统管理至关重要,本文将详细介绍Debian的常用命令,帮助读者更好地使... Debian是一个流行的linux发行版,它以其稳定性、强大的软件包管理和丰富的社区资源而闻名。在使用

关于Java内存访问重排序的研究

《关于Java内存访问重排序的研究》文章主要介绍了重排序现象及其在多线程编程中的影响,包括内存可见性问题和Java内存模型中对重排序的规则... 目录什么是重排序重排序图解重排序实验as-if-serial语义内存访问重排序与内存可见性内存访问重排序与Java内存模型重排序示意表内存屏障内存屏障示意表Int

SpringBoot使用minio进行文件管理的流程步骤

《SpringBoot使用minio进行文件管理的流程步骤》MinIO是一个高性能的对象存储系统,兼容AmazonS3API,该软件设计用于处理非结构化数据,如图片、视频、日志文件以及备份数据等,本文... 目录一、拉取minio镜像二、创建配置文件和上传文件的目录三、启动容器四、浏览器登录 minio五、