ust能力养成系列之(35):内存管理:以特性复制类型

2023-11-20 16:59

本文主要是介绍ust能力养成系列之(35):内存管理:以特性复制类型,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

简言之,Copy 和Clone 特性提现了类型在代码中的复制方式。

 

Copy

Copy特性通常是为栈上类型而实现的(The Copy trait is usually implemented for types that can be completely represented on the stack),也就是说,该特性自身没有任何部分存在于堆(heap)上。那是因为,如果在堆上,复制将是一个非常繁重的操作,其必须沿着堆进行向下值复制,而这会直接影响=赋值操作符的工作方式。如果一个类型实现了Copy特性,那么从一个变量到另一个变量的赋值也将隐式进行。

Copy是一个自动特性(auto trait),其在大多数栈数据类型(例如:原语premitives,和不可变引用immutable references,即&T)上自动实现。Copy的方式与C语言中的memcpy函数非常相似,其用于按位复制值。默认情况下,用户定义类型的Copy特性是没有实现的,因为Rust需要明确具体的复制行为,因此,开发人员必须选择实现特性。进一步,当开发人员想要对其类型实现复制时,首先要知道:Copy特性依赖于Clone特性。

就类型而言,如Vec<T>、String和可变引用,是没有特性Copy的实现的。要想复制这些值,需要使用更显式的Clone特性。

 

Clone

Clone特性用于显式复制,并附带一个clone方法,并以实现该方法来获得自身的副本。Clone 特性的定义如下:

pub trait Clone {fn clone(&self) -> Self;
}

该特性有一个名为clone的方法,该方法接一个不可变引用参数,即&self,并返回相同类型的新值。用户定义的类型,或任何需要提供复制自身能力的封装类型,应该通过实现clone方法来实现Clone特性。

但是,与赋值时进行隐式值复制的Copy类型不同,要复制Clone值,必须显式调用clone方法,该方法是一种通用的复制机制(duplication mechanism),Copy是其中的一种特殊情况,它总是按位复制。像String和Vec这种涉及大量复制的类型,只实现Clone特性。补充一下,智能指针类型还实现了Clonen特性,但只是复制指针和额外的元数据,比如指向相同堆数据的引用计数。

Clone特性为类型复制提供了灵活性,以下是众多的实例之一,我们看下:

// explicit_copy.rs#[derive(Clone, Debug)]
struct Dummy {items: u32
}fn main() {let a = Dummy { items: 54 };let b = a.clone();println!("a: {:?}, b: {:?}", a, b);
}

 

编译通过,结果如下

我们在derive属性中添加了Clone特性。这样,就可以调用clone 方法,来为变量 a来获得一个新的副本。

现在,我们看下类型复制的不同场景 ,以下是一些指导原则。

在类型上实现Copy特性,有些仅在栈上得以表征的小值,具体看来:

  • 如果该类型仅依赖于在其上实现了Copy的其他类型;其隐式实现Copy特性
  • Copy特性隐式影响着赋值操作符=的工作方式。为外部可见类型应用Copy特性,需要考虑其对赋值操作符的影响。如果在开发的早期,在类型涉及一个Copy特性,然后删除,那么这将影响分配该类型值的每个点,以此可以很容易的破坏基于此特性的一个API。

 

在类型上实现Clone特性:

  • Clone trait仅声明一个需要显式调用的clone方法
  • 如果类型还在堆上包含一个值为其部分表征,那么选择实现Clone,以显式复制堆数据
  • 如果正在实现一个智能指针类型,比如引用计数类型,那么应该在相关类型上实现Clone,以便只复制栈上的指针

 

所有权之实践(Ownership in action)

除了之前介绍过的let绑定的例子外,还有其他一些地方可以看到所有权也在生效,开发者对此应予以一定的重视。

 

函数(Functions)

如果你传递参数给函数,同样的所有权规则生效:

// ownership_functions.rsfn take_the_n(n: u8) { }fn take_the_s(s: String) { }fn main() { let n = 5; let s = String::from("string"); take_the_n(n); take_the_s(s); println!("n is {}", n); println!("s is {}", s); 
} 

编译未通过,报错如下:

String没有实现Copy特性,所以值的所有权被移动到take_the_s函数中。当该函数返回时,该值的作用域结束,在s上调用drop函数,释放s所使用的堆内存。因此,在函数调用后,s不能再被使用。然而,由于String实现了Clone,可以通过在函数调用处添加.clone()调用来使代码正常工作:

take_the_s(s.clone());

这里的take_the_n工作良好,因为u8(是一个原语类型)实现了Copy。

也就是说,在将move类型传递给函数后,不能在以后使用该值。如果要使用该值,必须clone该类型,并向函数发送一个副本。现在,如果我们只需要对变量s进行读访问,另一种方法是将字符串s传递回main。代码可以如下所示:我们为take_the_s函数添加了返回类型,并将传递的字符串s返回给调用者。在main中,在s中接收。这样,main的最后一行代码就可以完成任务了。

// ownership_functions_back.rsfn take_the_n(n: u8) { }fn take_the_s(s: String) -> String {println!("inside function {}", s);s
}fn main() { let n = 5; let s = String::from("string"); take_the_n(n); let s = take_the_s(s); println!("n is {}", n); println!("s is {}", s); 
} 

编译通过,结果如下:

Match表达式(Match expressions)

在Match表达式中,移动类型也是默认移动的,如下面的代码所示:

// ownership_match.rs#[derive(Debug)]
enum Food {Cake,Pizza,Salad
}#[derive(Debug)]
struct Bag {food: Food
}fn main() {let bag = Bag { food: Food::Cake };match bag.food {Food::Cake => println!("I got cake"),a => println!("I got {:?}", a)}println!("{:?}", bag);
}

在前面的代码中,我们创建了一个Bag实例并将其分配给bag。接下来,我们匹配它的food字段并打印一些文本。稍后,用println!打印bag。编译时会得到以下错误:

可以清楚看到,错误消息表明bag已经被match表达式中的a变量移动和使用。这将使变量bag失效,无法进一步使用。当稍后了解到借用(borrowing)的概念时,我们会了解如何来使这些代码工作。

 

方法(Methods)

在impl块中,任何以self作为第一个参数的方法都拥有该方法所调用值的所有权。这意味着在对该值调用过该方法后,将不能再次使用该值。如下面的代码所示:

// ownership_methods.rsstruct Item(u32);impl Item {fn new() -> Self {Item(1024)}fn take_item(self) {// does nothing} 
}fn main() {let it = Item::new();it.take_item();println!("{}", it.0);
}

编译未通过,报错如下:

take_item是一个实例方法,它将self作为第一个参数。在调用之后,它被移动到方法内部,并在函数作用域结束时释放。于是之后不能再用了。依然,当讲到借用概念时,我们会使这些代码重新工作。

 

闭包中的所有权(Ownership in closures)

类似的事情也发生在闭包上。考虑以下代码:

// ownership_closures.rs#[derive(Debug)]
struct Foo;fn main() {let a = Foo;let closure = || {let b = a;    };println!("{:?}", a);
}

不难猜到,默认情况下,Foo在闭包内的所有权在赋值时转移到b,不能再次访问a。当编译上述代码时,得到以下输出:

要获得a的副本,可以在闭包中调用a.clone()并将其赋值给b,或者在闭包前放置一个move关键字,如下所示:

#[derive(Debug,Clone)]
struct Foo;fn main() {let a = Foo;let closure = || {let b = a.clone();    };println!("{:?}", a);
}

或者

#[derive(Debug,Copy)]
struct Foo;fn main() {let a = Foo;let closure = move || {let b = a;    };println!("{:?}", a);
}

皆可通过编译,结果如下

通过以上这些实例,可以看到所有权规则相当严格,因为它只允许使用一个类型一次。如果函数只需要对一个值进行读访问,则需要从函数中返回该值,或者在将该值传递给函数之前clone该值。如果该类型没有实现Clone,则后一种方法可能不可行。克隆一下类型,似乎很容易绕过所有权原则,但它损坏了零成本承诺的全部意义,因为Clone总是要进行类型复制,这可能会涉及使用内存分配器的APIs这等系统级别调用,显然是颇为消耗系统资源的。

 

结语

随着move语义和所有权规则的生效,在Rust中编写程序很快就会变得非常笨拙。幸运的是,我们有了借用和引用类型的概念,可以放松对规则施加的限制,但仍然可在编译时保持所有权。

 

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

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

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

这篇关于ust能力养成系列之(35):内存管理:以特性复制类型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

软考系统规划与管理师考试证书含金量高吗?

2024年软考系统规划与管理师考试报名时间节点: 报名时间:2024年上半年软考将于3月中旬陆续开始报名 考试时间:上半年5月25日到28日,下半年11月9日到12日 分数线:所有科目成绩均须达到45分以上(包括45分)方可通过考试 成绩查询:可在“中国计算机技术职业资格网”上查询软考成绩 出成绩时间:预计在11月左右 证书领取时间:一般在考试成绩公布后3~4个月,各地领取时间有所不同

安全管理体系化的智慧油站开源了。

AI视频监控平台简介 AI视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。用户只需在界面上进行简单的操作,就可以实现全视频的接入及布控。摄像头管理模块用于多种终端设备、智能设备的接入及管理。平台支持包括摄像头等终端感知设备接入,为整个平台提

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。