Rust常用特型之Drop特型

2024-03-11 16:44
文章标签 rust 常用 drop 特型

本文主要是介绍Rust常用特型之Drop特型,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Rust常用特型之Drop特型.md在Rust标准库中,存在很多常用的工具类特型,它们能帮助我们写出更具有Rust风格的代码。

今天,我们主要学习Drop特型。

(注:本文更多的是对《Programing Rust 2nd Edition》的自己翻译和理解,并不是原创)

一、什么是Drop

当一个值不再拥有owner时(在Rust中每个值都有一个owner,并且最多只有一个owner),我们说Rust释放/清理(Drop)了该值。释放一个值通常意味着也需要一并释放它占用的其它资源,例如堆存储。释放可以发生在多种场合:例如变量超出作用域,表达式语句的结尾,截断一个向量并移除末尾的值等。

接下来的内容中,清理和释放表达的是同一个含义,均为drop的意思。

通常情况下,Rust会自动为你清理值。例如如下代码:

struct Appellation {name: String,nicknames: Vec<String>
}

这里我们来复习一下Vec<T>的有关知识。

一个Vec<T>由三个值构成, 第一个值是指针,它指向在堆上为元素分配的缓冲区。 该缓冲区由Vec<T>本身拥有。第二值是缓冲区的容量Cap。第三个值是当前元素的个数length。它是一个胖指针。当缓冲区的大小达到它的容量时,再增加元素会重新分配一个更大的缓冲区,并将原来的元素复制过去,同时更新向量的指针,容量和长度值,最后释放旧的缓冲区。

一个Appellation对象即包含了堆上的字符串内容(对应的name字段),又包含了堆上的向量元素缓冲区(对应nicknames字段)。当这个对象释放时,Rust会小心清理所有资源,并不需要你自己做任何处理。然而,如果你愿意,你也可以通过实现std::ops::Drop特型来自定义你的类型的清理方式这里为什么有个你的类型呢?因为Rust不允许特型和类型都是外部的,必须有一个是本地的。此时Drop特型已经是外来的(相对于你的代码),因此类型必须是本地定义的。

Drop特型的定义为:

trait Drop {fn drop(&mut self);
}

个人理解,未必正确

我们可以看到,该特型仅有一个drop函数,注意它的参数类型是&mut,因为我们要做相关清理工作,因此必须是可变的。如果参数是mut self会怎么样?那么相当于值转移到本函数中了,在本函数处理完毕后该值的owner就不存在了,此时又到了调用drop的场景,从而形成无限循环,所以参数类型必定为&mut

二、Drop特型的实现

当一个值被清理时,如果它实现了Drop特型,那么Rust会自动调用它的drop方法。该调用发生在清理它的内部元素或者字段之前。这说明用户自定义的drop函数有第一优先权。当然这种隐匿调用也是调用drop函数的唯一方式,如果你手动调用它,那么Rust会标记为一个错误。

这里也印证了上面提到的drop函数的参数类型&mut,因为发生在清理它的内部元素之前,所以该值在此时必须保留,所以不能是mut self。也正因为如此,这个值一定是初始化过的(应该是变量初始化过)。

上面Appellation类型的一个示例Drop实现代码为:

impl Drop for Appellation {fn drop(&mut self) {print!("Dropping {}", self.name);if !self.nicknames.is_empty() {print!(" (AKA {})", self.nicknames.join(", "));}println!("");}
}

假定实现为上述代码,那么我们可以接下来写一段测试代码:

{let mut a = Appellation {name: "Zeus".to_string(),nicknames: vec!["cloud collector".to_string(),"king of the gods".to_string()]};println!("before assignment");a = Appellation { name: "Hera".to_string(), nicknames: vec![]};println!("at end of block");
}

那么运行得到的结果是什么呢?我们一行一行来分析代码:

  • 1-6行,定义了一个类型为 Appellation 的mut变量a ,它的值在定义时已经初始化了
  • 第7行,打印开始重新赋值信息before assignment并换行。
  • 第8行,将a重新赋值,此时a原来的值被抛弃了,没有owner了,因此符合清理的条件,Rust会自动对其进行清理,在该值上调用drop函数
  • drop函数首先打印值的name,这里应该是Dropping Zeus。注意这里是print!,未换行。
  • 接下来,因为nicknames不为空,将它的元素使用,连接起来,所以应该为 (AKA cloud collector,king of the gods)。注意这里是print!,未换行,因此是接在Dropping Zeus之后。
  • 接下来println!("");目的是产生换行。
  • drop函数调用完毕,接下来回到示例代码第9行,打印at end of block
  • 第10行,示例代码结束,变量a超过作用域,在此释放,也会调用其drop函数。
  • 再次回到drop函数,打印对象名称,此时应该为Dropping Hera
  • 因为第二个Appellation值的nicknames字段为空向量,所以不再打印AKA相关。
  • 再次换行。

最终输出结果为:

before assignment
Dropping Zeus (AKA cloud collector, king of the gods)
at end of block
Dropping Hera

上面的代码中,类型为Appellation的变量a前后有两个不同的值,因此触发了两次清理。第一次清理发生在重新赋值时,此时第一个值被抛弃,变成了无owner,所以触发清理。第二次发生在代码块结束 ,此时a超出作用域,也触发清理。

可以看到,我们的清理并没有清除掉内部元素占用的资源,这是Rust会在接下来自动处理的,我们的工作主要是作一些额外的处理。

针对这个问题,书中已经给了明确答案。Rust自动清理内部元素,而内部元素也会自动清理自己。例如Vec类型也实现了Drop特型,它会清理掉它的内部元素并释放它占用的堆上的缓冲区。字符串内部使用Vec<u8>来保存它的文本,因此字符串并不需要自己实现Drop特型(Vec<T>实现了就可以),向量本身来处理这些字符的释放。相同的原则应用于Appellation值,向量的Drop实现会自动释放它的元素。对于 Appellation值本身,它也有一个owner,它可以是本地临时变量或者某些数据结构,这个变量对释放它负责。

注意:

当一个变量的值被移走时,该变量就是未初始化的,因此在超过作用域时并不会触发drop,没有值需要清理。切记,清理的是值不是变量。

下面的一段代码:

let p;{let q = Appellation { name: "Cardamine hirsuta".to_string(),nicknames: vec!["shotweed".to_string(),"bittercress".to_string()] };if complicated_condition() {p = q;}
}
println!("Sproing! What was that?");

根据complicated_condition返回值的不同,p或者q其中的一个在代码结束时会拥有这个Appellation值,另一个变量是未初始化。这也决定了他们是在最后的println!之前还是之后drop(这是因为q的作用域在println!之前结束而p的作用域在这之后结束)。虽然在Rust中一个值可以从一个变量移到另一个变量,但是只会清理一次。

通常情况下,你不需要给自己定义的类型实现Drop特型,除非它拥有了Rust所不能自动处理的资源。例如,在Unix系统中,Rust标准为使用如下的内部结构来代表操作系统文件描述:

struct FileDesc {fd: c_int,
}

其中fd字段代表的文件描述数字在程序结束的时候应该关掉。标准库因此为之实现了Drop特型来关掉它。

impl Drop for FileDesc {fn drop(&mut self) {let _ = unsafe { libc::close(self.fd) };}
}

这里,libc::close是C语言库的close函数的Rust名字,Rust只能在unsafe代码块中调用C语言的函数。

知识点:

如果一个类型实现了Drop特型,那么它不能再实现Copy特型。如果一个类型是Copy类型,那么意味着简单的字节复制就够了,这样可能会导致两个变量会拥有同一块数据。但是如果两个变量都面临清理时,相同的数据就会清理两次,这是一个错误。就好像上面的FileDesc例子,如果它实现了Copy特型,那么另一个变量也会关闭相同的fd数字,显然这是一个错误。

进一步思考,如果把Copy换成Clone呢?经过测试是没有问题的。

use std::ops::Drop;
// A unit struct without resources
#[derive(Debug, Clone)]
struct Unit;impl Drop for Unit {fn drop(&mut self) {println!("in drop");}
}fn main() {let a = Unit;let b = a.clone();println!("over:{:?}",b);
}

运行结果为:

over:Unit
in drop
in drop

有人说那如果把FileDesc设计为实现Clone特型不一样么?其实还真不一样,因为fd字段的排它性,所以把它设计为Clone是错误的。只有可以复制的资源才能设计为实现Clone特型,这个问题其实是Clone特型的设计问题了,而不是Drop特型的问题。

有人说如果两个变量都包含对同一块数据的引用,那么是不是清理两次呢?显然不是,引用不拥有值,不会触发清理。

标准前置还包含了一个drip函数用来清理一个值,但是它的定义相当魔幻:

fn drop<T>(_x: T) { }

从代码中可以看出,它接收一个值并且获得了该值的owner。在函数结束时_x超出了作用域而会被Rust正常的清理掉。这里只是提供了一个便利功能,并不是手动调用值的drop函数。

这篇关于Rust常用特型之Drop特型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

springboot项目中常用的工具类和api详解

《springboot项目中常用的工具类和api详解》在SpringBoot项目中,开发者通常会依赖一些工具类和API来简化开发、提高效率,以下是一些常用的工具类及其典型应用场景,涵盖Spring原生... 目录1. Spring Framework 自带工具类(1) StringUtils(2) Coll

Java String字符串的常用使用方法

《JavaString字符串的常用使用方法》String是JDK提供的一个类,是引用类型,并不是基本的数据类型,String用于字符串操作,在之前学习c语言的时候,对于一些字符串,会初始化字符数组表... 目录一、什么是String二、如何定义一个String1. 用双引号定义2. 通过构造函数定义三、St

Linux上设置Ollama服务配置(常用环境变量)

《Linux上设置Ollama服务配置(常用环境变量)》本文主要介绍了Linux上设置Ollama服务配置(常用环境变量),Ollama提供了多种环境变量供配置,如调试模式、模型目录等,下面就来介绍一... 目录在 linux 上设置环境变量配置 OllamPOgxSRJfa手动安装安装特定版本查看日志在

Java常用注解扩展对比举例详解

《Java常用注解扩展对比举例详解》:本文主要介绍Java常用注解扩展对比的相关资料,提供了丰富的代码示例,并总结了最佳实践建议,帮助开发者更好地理解和应用这些注解,需要的朋友可以参考下... 目录一、@Controller 与 @RestController 对比二、使用 @Data 与 不使用 @Dat

Mysql中深分页的五种常用方法整理

《Mysql中深分页的五种常用方法整理》在数据量非常大的情况下,深分页查询则变得很常见,这篇文章为大家整理了5个常用的方法,文中的示例代码讲解详细,大家可以根据自己的需求进行选择... 目录方案一:延迟关联 (Deferred Join)方案二:有序唯一键分页 (Cursor-based Paginatio

Python实现常用文本内容提取

《Python实现常用文本内容提取》在日常工作和学习中,我们经常需要从PDF、Word文档中提取文本,本文将介绍如何使用Python编写一个文本内容提取工具,有需要的小伙伴可以参考下... 目录一、引言二、文本内容提取的原理三、文本内容提取的设计四、文本内容提取的实现五、完整代码示例一、引言在日常工作和学

Redis中的常用的五种数据类型详解

《Redis中的常用的五种数据类型详解》:本文主要介绍Redis中的常用的五种数据类型详解,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Redis常用的五种数据类型一、字符串(String)简介常用命令应用场景二、哈希(Hash)简介常用命令应用场景三、列表(L

python中time模块的常用方法及应用详解

《python中time模块的常用方法及应用详解》在Python开发中,时间处理是绕不开的刚需场景,从性能计时到定时任务,从日志记录到数据同步,时间模块始终是开发者最得力的工具之一,本文将通过真实案例... 目录一、时间基石:time.time()典型场景:程序性能分析进阶技巧:结合上下文管理器实现自动计时

C#中的 Dictionary常用操作

《C#中的Dictionary常用操作》C#中的DictionaryTKey,TValue是用于存储键值对集合的泛型类,允许通过键快速检索值,并且具有唯一键、动态大小和无序集合的特性,常用操作包括添... 目录基本概念Dictionary的基本结构Dictionary的主要特性Dictionary的常用操作

Python中常用的四种取整方式分享

《Python中常用的四种取整方式分享》在数据处理和数值计算中,取整操作是非常常见的需求,Python提供了多种取整方式,本文为大家整理了四种常用的方法,希望对大家有所帮助... 目录引言向零取整(Truncate)向下取整(Floor)向上取整(Ceil)四舍五入(Round)四种取整方式的对比综合示例应