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

相关文章

Linux创建服务使用systemctl管理详解

《Linux创建服务使用systemctl管理详解》文章指导在Linux中创建systemd服务,设置文件权限为所有者读写、其他只读,重新加载配置,启动服务并检查状态,确保服务正常运行,关键步骤包括权... 目录创建服务 /usr/lib/systemd/system/设置服务文件权限:所有者读写js,其他

Python内存管理机制之垃圾回收与引用计数操作全过程

《Python内存管理机制之垃圾回收与引用计数操作全过程》SQLAlchemy是Python中最流行的ORM(对象关系映射)框架之一,它提供了高效且灵活的数据库操作方式,本文将介绍如何使用SQLAlc... 目录安装核心概念连接数据库定义数据模型创建数据库表基本CRUD操作创建数据读取数据更新数据删除数据查

在Node.js中使用.env文件管理环境变量的全过程

《在Node.js中使用.env文件管理环境变量的全过程》Node.js应用程序通常依赖于环境变量来管理敏感信息或配置设置,.env文件已经成为一种流行的本地管理这些变量的方法,本文将探讨.env文件... 目录引言为什么使php用 .env 文件 ?如何在 Node.js 中使用 .env 文件最佳实践引

C#利用Free Spire.XLS for .NET复制Excel工作表

《C#利用FreeSpire.XLSfor.NET复制Excel工作表》在日常的.NET开发中,我们经常需要操作Excel文件,本文将详细介绍C#如何使用FreeSpire.XLSfor.NET... 目录1. 环境准备2. 核心功能3. android示例代码3.1 在同一工作簿内复制工作表3.2 在不同

python库pydantic数据验证和设置管理库的用途

《python库pydantic数据验证和设置管理库的用途》pydantic是一个用于数据验证和设置管理的Python库,它主要利用Python类型注解来定义数据模型的结构和验证规则,本文给大家介绍p... 目录主要特点和用途:Field数值验证参数总结pydantic 是一个让你能够 confidentl

Python函数的基本用法、返回值特性、全局变量修改及异常处理技巧

《Python函数的基本用法、返回值特性、全局变量修改及异常处理技巧》本文将通过实际代码示例,深入讲解Python函数的基本用法、返回值特性、全局变量修改以及异常处理技巧,感兴趣的朋友跟随小编一起看看... 目录一、python函数定义与调用1.1 基本函数定义1.2 函数调用二、函数返回值详解2.1 有返

k8s容器放开锁内存限制问题

《k8s容器放开锁内存限制问题》nccl-test容器运行mpirun时因NCCL_BUFFSIZE过大导致OOM,需通过修改docker服务配置文件,将LimitMEMLOCK设为infinity并... 目录问题问题确认放开容器max locked memory限制总结参考:https://Access

SpringBoot 多环境开发实战(从配置、管理与控制)

《SpringBoot多环境开发实战(从配置、管理与控制)》本文详解SpringBoot多环境配置,涵盖单文件YAML、多文件模式、MavenProfile分组及激活策略,通过优先级控制灵活切换环境... 目录一、多环境开发基础(单文件 YAML 版)(一)配置原理与优势(二)实操示例二、多环境开发多文件版

Python中Json和其他类型相互转换的实现示例

《Python中Json和其他类型相互转换的实现示例》本文介绍了在Python中使用json模块实现json数据与dict、object之间的高效转换,包括loads(),load(),dumps()... 项目中经常会用到json格式转为object对象、dict字典格式等。在此做个记录,方便后续用到该方

C#文件复制异常:"未能找到文件"的解决方案与预防措施

《C#文件复制异常:未能找到文件的解决方案与预防措施》在C#开发中,文件操作是基础中的基础,但有时最基础的File.Copy()方法也会抛出令人困惑的异常,当targetFilePath设置为D:2... 目录一个看似简单的文件操作问题问题重现与错误分析错误代码示例错误信息根本原因分析全面解决方案1. 确保