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 Boot 配置文件之类型、加载顺序与最佳实践记录

《SpringBoot配置文件之类型、加载顺序与最佳实践记录》SpringBoot的配置文件是灵活且强大的工具,通过合理的配置管理,可以让应用开发和部署更加高效,无论是简单的属性配置,还是复杂... 目录Spring Boot 配置文件详解一、Spring Boot 配置文件类型1.1 applicatio

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

新特性抢先看! Ubuntu 25.04 Beta 发布:Linux 6.14 内核

《新特性抢先看!Ubuntu25.04Beta发布:Linux6.14内核》Canonical公司近日发布了Ubuntu25.04Beta版,这一版本被赋予了一个活泼的代号——“Plu... Canonical 昨日(3 月 27 日)放出了 Beta 版 Ubuntu 25.04 系统镜像,代号“Pluc

nvm如何切换与管理node版本

《nvm如何切换与管理node版本》:本文主要介绍nvm如何切换与管理node版本问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录nvm切换与管理node版本nvm安装nvm常用命令总结nvm切换与管理node版本nvm适用于多项目同时开发,然后项目适配no

Python如何查看数据的类型

《Python如何查看数据的类型》:本文主要介绍Python如何查看数据的类型方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录python查看数据的类型1. 使用 type()2. 使用 isinstance()3. 检查对象的 __class__ 属性4.

Python容器类型之列表/字典/元组/集合方式

《Python容器类型之列表/字典/元组/集合方式》:本文主要介绍Python容器类型之列表/字典/元组/集合方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1. 列表(List) - 有序可变序列1.1 基本特性1.2 核心操作1.3 应用场景2. 字典(D

Python如何在Word中生成多种不同类型的图表

《Python如何在Word中生成多种不同类型的图表》Word文档中插入图表不仅能直观呈现数据,还能提升文档的可读性和专业性,本文将介绍如何使用Python在Word文档中创建和自定义各种图表,需要的... 目录在Word中创建柱形图在Word中创建条形图在Word中创建折线图在Word中创建饼图在Word

SpringBoot接收JSON类型的参数方式

《SpringBoot接收JSON类型的参数方式》:本文主要介绍SpringBoot接收JSON类型的参数方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、jsON二、代码准备三、Apifox操作总结一、JSON在学习前端技术时,我们有讲到过JSON,而在

Redis实现RBAC权限管理

《Redis实现RBAC权限管理》本文主要介绍了Redis实现RBAC权限管理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1. 什么是 RBAC?2. 为什么使用 Redis 实现 RBAC?3. 设计 RBAC 数据结构

Redis 内存淘汰策略深度解析(最新推荐)

《Redis内存淘汰策略深度解析(最新推荐)》本文详细探讨了Redis的内存淘汰策略、实现原理、适用场景及最佳实践,介绍了八种内存淘汰策略,包括noeviction、LRU、LFU、TTL、Rand... 目录一、 内存淘汰策略概述二、内存淘汰策略详解2.1 ​noeviction(不淘汰)​2.2 ​LR