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集成Druid实现数据源管理与监控的详细步骤

《SpringBoot集成Druid实现数据源管理与监控的详细步骤》本文介绍如何在SpringBoot项目中集成Druid数据库连接池,包括环境搭建、Maven依赖配置、SpringBoot配置文件... 目录1. 引言1.1 环境准备1.2 Druid介绍2. 配置Druid连接池3. 查看Druid监控

Knife4j+Axios+Redis前后端分离架构下的 API 管理与会话方案(最新推荐)

《Knife4j+Axios+Redis前后端分离架构下的API管理与会话方案(最新推荐)》本文主要介绍了Swagger与Knife4j的配置要点、前后端对接方法以及分布式Session实现原理,... 目录一、Swagger 与 Knife4j 的深度理解及配置要点Knife4j 配置关键要点1.Spri

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java内存分配与JVM参数详解(推荐)

《Java内存分配与JVM参数详解(推荐)》本文详解JVM内存结构与参数调整,涵盖堆分代、元空间、GC选择及优化策略,帮助开发者提升性能、避免内存泄漏,本文给大家介绍Java内存分配与JVM参数详解,... 目录引言JVM内存结构JVM参数概述堆内存分配年轻代与老年代调整堆内存大小调整年轻代与老年代比例元空

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

Conda虚拟环境的复制和迁移的四种方法实现

《Conda虚拟环境的复制和迁移的四种方法实现》本文主要介绍了Conda虚拟环境的复制和迁移的四种方法实现,包括requirements.txt,environment.yml,conda-pack,... 目录在本机复制Conda虚拟环境相同操作系统之间复制环境方法一:requirements.txt方法

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

使用jenv工具管理多个JDK版本的方法步骤

《使用jenv工具管理多个JDK版本的方法步骤》jenv是一个开源的Java环境管理工具,旨在帮助开发者在同一台机器上轻松管理和切换多个Java版本,:本文主要介绍使用jenv工具管理多个JD... 目录一、jenv到底是干啥的?二、jenv的核心功能(一)管理多个Java版本(二)支持插件扩展(三)环境隔

Redis过期删除机制与内存淘汰策略的解析指南

《Redis过期删除机制与内存淘汰策略的解析指南》在使用Redis构建缓存系统时,很多开发者只设置了EXPIRE但却忽略了背后Redis的过期删除机制与内存淘汰策略,下面小编就来和大家详细介绍一下... 目录1、简述2、Redis http://www.chinasem.cn的过期删除策略(Key Expir

Linux使用scp进行远程目录文件复制的详细步骤和示例

《Linux使用scp进行远程目录文件复制的详细步骤和示例》在Linux系统中,scp(安全复制协议)是一个使用SSH(安全外壳协议)进行文件和目录安全传输的命令,它允许在远程主机之间复制文件和目录,... 目录1. 什么是scp?2. 语法3. 示例示例 1: 复制本地目录到远程主机示例 2: 复制远程主