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

相关文章

使用Redis实现会话管理的示例代码

《使用Redis实现会话管理的示例代码》文章介绍了如何使用Redis实现会话管理,包括会话的创建、读取、更新和删除操作,通过设置会话超时时间并重置,可以确保会话在用户持续活动期间不会过期,此外,展示了... 目录1. 会话管理的基本概念2. 使用Redis实现会话管理2.1 引入依赖2.2 会话管理基本操作

MySQL快速复制一张表的四种核心方法(包括表结构和数据)

《MySQL快速复制一张表的四种核心方法(包括表结构和数据)》本文详细介绍了四种复制MySQL表(结构+数据)的方法,并对每种方法进行了对比分析,适用于不同场景和数据量的复制需求,特别是针对超大表(1... 目录一、mysql 复制表(结构+数据)的 4 种核心方法(面试结构化回答)方法 1:CREATE

MyBatis中的两种参数传递类型详解(示例代码)

《MyBatis中的两种参数传递类型详解(示例代码)》文章介绍了MyBatis中传递多个参数的两种方式,使用Map和使用@Param注解或封装POJO,Map方式适用于动态、不固定的参数,但可读性和安... 目录✅ android方式一:使用Map<String, Object>✅ 方式二:使用@Param

C# WebAPI的几种返回类型方式

《C#WebAPI的几种返回类型方式》本文主要介绍了C#WebAPI的几种返回类型方式,包括直接返回指定类型、返回IActionResult实例和返回ActionResult,文中通过示例代码介绍的... 目录创建 Controller 和 Model 类在 Action 中返回 指定类型在 Action

使用Python实现高效复制Excel行列与单元格

《使用Python实现高效复制Excel行列与单元格》在日常办公自动化或数据处理场景中,复制Excel中的单元格、行、列是高频需求,下面我们就来看看如何使用FreeSpire.XLSforPython... 目录一、环境准备:安装Free Spire.XLS for python二、核心实战:复制 Exce

Java多种文件复制方式以及效率对比分析

《Java多种文件复制方式以及效率对比分析》本文总结了Java复制文件的多种方式,包括传统的字节流、字符流、NIO系列、第三方包中的FileUtils等,并提供了不同方式的效率比较,同时,还介绍了遍历... 目录1 背景2 概述3 遍历3.1listFiles()3.2list()3.3org.codeha

使用Python实现在PDF中添加、导入、复制、移动与删除页面

《使用Python实现在PDF中添加、导入、复制、移动与删除页面》在日常办公和自动化任务中,我们经常需要对PDF文件进行页面级的编辑,使用Python,你可以轻松实现这些操作,而无需依赖AdobeAc... 目录1. 向 PDF 添加空白页2. 从另一个 PDF 导入页面3. 删除 PDF 中的页面4. 在

Java JAR 启动内存参数配置指南(从基础设置到性能优化)

《JavaJAR启动内存参数配置指南(从基础设置到性能优化)》在启动Java可执行JAR文件时,合理配置JVM内存参数是保障应用稳定性和性能的关键,本文将系统讲解如何通过命令行参数、环境变量等方式... 目录一、核心内存参数详解1.1 堆内存配置1.2 元空间配置(MetASPace)1.3 线程栈配置1.

python中的鸭子类型详解

《python中的鸭子类型详解》鸭子类型是Python动态类型系统的灵魂,它通过强调“行为”而非“类型”,赋予了代码极大的灵活性和表现力,本文给大家详细介绍python中的鸭子类型,感兴趣的朋友一起看... 目录1. 核心思想:什么是鸭子类型?2. 与“传统”静态类型语言的对比3. python 中无处不在

Elasticsearch 的索引管理与映射配置实战指南

《Elasticsearch的索引管理与映射配置实战指南》在本文中,我们深入探讨了Elasticsearch中索引与映射的基本概念及其重要性,通过详细的操作示例,我们了解了如何创建、更新和删除索引,... 目录一、索引操作(一)创建索引(二)删除索引(三)关闭索引(四)打开索引(五)索引别名二、映射操作(一