Begging_Rust(译):丢弃,移动和复制(第二十一章) - Go语言中文社区

Begging_Rust(译):丢弃,移动和复制(第二十一章)


在本章中,您将学习:
•为什么对象的确定性和隐式破坏是Rust的一大优点

•对象所有权的概念

•为什么自定义析构函数可能有用,以及如何创建它们

•三种赋值语义:共享语义,复制语义和移动语义

•为什么隐式共享语义不利于软件的正确性

•为什么移动语义可能比复制语义具有更好的性能

•为什么某些类型需要复制语义而其他类型不需要,以及如何指定

•为什么某些类型需要不可克隆,以及如何指定

确定性破坏

到目前为止,我们已经看到了几种在堆栈和堆中分配对象的方法:

•临时表达式,在堆栈中分配;

•在堆栈中分配的变量(包括数组);

•在堆栈中分配的函数和闭包参数;

•Box对象,在堆栈中分配引用,以及在堆中分配的引用对象;

•动态字符串和集合(包括向量),在堆栈中分配标头,以及在堆中分配的数据。

分配此类对象的实际时刻很难预测,因为它取决于编译器优化。 所以,让我们考虑这种分配的概念瞬间。

从概念上讲,每个堆栈分配都在相应的表达式首次出现在代码中时发生。 所以:

•临时表达式,变量和数组在代码中出现时会被分配;

•调用函数/闭包时分配函数和闭包参数;

•Box-es,动态字符串和集合标题在代码中出现时会被分配。

当需要这样的数据时,每个堆分配都会发生。 所以:

•Box-ed对象由Box :: new函数分配;

•将一些字符添加到字符串时,将分配动态字符串字符;

•将一些数据添加到集合时,将分配集合内容。

所有这些与大多数编程语言没有什么不同。 什么时候发生数据项的重新分配从概念上讲,在Rust中,当这样的数据项不再可访问时,它会自动发生。 所以:

•当包含它们的语句结束时(即,在下一个分号或当前范围结束时),将释放临时表达式;

•当包含其声明的范围结束时,变量(包括数组)将被释放;

•当函数/闭包块结束时,将释放函数和闭包参数;
•当包含其声明的范围结束时,将取消分配框对象;

•动态字符串中包含的字符在从字符串中删除时会被释放,或者在字符串被释放时被解除分配;

•集合中包含的项目在从集合中删除时会被释放,或者在集合被释放时取消分配。

这是一个将Rust与大多数编程语言区分开来的概念。 在具有临时对象或堆栈分配对象的任何语言中,此类对象将自动释放。 但堆分配的对象释放因不同的语言而异。

在某些语言中,如Pascal,C和C ++,堆对象通常只通过调用“free”或“delete”等函数显式释放。 在Java,JavaScript,C#和Python等其他语言中,堆对象在不再可访问时不会立即释放,但是有一个例程,定期运行,找到无法访问的堆对象并释放它们。 这种机制被称为“垃圾收集”,因为它类似于城市清洁系统:当一些垃圾堆积起来时,它定期清理城镇。

因此,在C ++和类似语言中,堆释放既是确定性的,也是显式的。它是确定性的,因为它发生在明确定义的源代码位置;它是显式的,因为它要求程序员编写一个特定的释放语句。确定性是好的,因为它具有更好的性能,并且它允许程序员更好地控制计算机中正在发生的事情。但要明确是不好的,因为如果错误地执行解除分配,会导致令人讨厌的错误。

相反,在Java和类似语言中,堆释放既是非确定性的,也是隐含的。它是非确定性的,因为它发生在未知的执行时刻;它是隐式的,因为它不需要特定的释放语句。不确定性是坏的,但隐含是好的。与Rust这两种技术不同,通常,堆释放既是确定性的,也是隐式的,这是Rust相对于其他语言的一大优势。

这是可能的,因为基于“所有权”的概念的以下机制。

所有权

我们来介绍“拥有”一词。 在计算机科学中,对于标识符或对象A,“拥有”对象B,意味着A负责解除分配B,这意味着两件事:

•只有A可以解除分配B.

•当A变得无法访问时,A必须解除分配B.

在Rust中,没有明确的释放机制,因此这个定义可以改写为“A拥有B意味着当且仅当A变得无法访问时才释放B”。

let mut a = 3;
a = 4;
let b = vec![11, 22, 33, 44, 55];

在此程序中,变量a拥有最初包含值3的对象,因为当a超出其范围,因此无法访问时,最初具有值3的对象将被释放。 我们也可以说“a是对象的所有者,其初始值为3”。 虽然,我们不应该说“拥有3”,因为3是一个值,而不是一个对象; 只有对象可以拥有。 在内存中,可以有许多具有值3的对象,并且只拥有其中一个。 在前一个程序的第二个语句中,此对象的值更改为4; 但它的所有权没有改变:仍然拥有它。

在最后一个语句中,b被初始化为五个项的向量。 这种矢量有一个标题和一个数据缓冲区; 标头实现为包含三个成员的结构:指向数据缓冲区的指针和两个数字; 数据缓冲区包含五个项目,可能还有一些额外的空间。 这里我们可以说“b拥有向量的头部,并且包含在向量头部中的指针拥有数据缓冲区”。 实际上,当b超出其范围时,向量头被释放; 当该向量头被释放时,其包含的指针变得无法访问; 当这种向量表示非空向量时,也释放包含向量项的缓冲区。

但是,并非每个引用都拥有一个对象。

let a = 3;
{
let a_ref = &a;
}
print!("{}, a);

这里,a_ref变量拥有一个引用,但该引用不拥有任何内容。 实际上,在嵌套块的末尾,a_ref变量超出其范围,因此引用被释放,但引用的对象(值为3的数字)不应立即释放,因为它必须是 打印在最后一个声明。

为了确保在不再引用时自动释放每个对象,Rust有一个简单的规则,即在每个执行的瞬间,每个对象必须只有一个“所有者”,不多也不少。 取消分配该所有者后,将释放该对象本身。 如果有多个所有者,则可以多次释放该对象,但这是不允许的。 如果没有所有者,则永远不会释放该对象,这是一个名为“内存泄漏”的错误。

析构函数

我们看到对象创建有两个步骤:分配对象所需的内存空间,并使用值初始化此类空间。 对于复杂对象,初始化非常复杂,通常会使用一个函数。 这些函数被命名为“构造函数”,因为它们“构造”新对象。

我们刚刚看到,当一个对象被释放时,可能会发生一些相当复杂的事情。 如果该对象引用堆中的其他对象,则可能发生级联的解除分配。 因此,即使对象的“破坏”也可能需要由名为“析构函数”的函数执行。

通常,析构函数是语言或标准库的一部分,但有时您可能需要在取消分配对象时执行一些清理代码,因此您需要编写析构函数。

struct CommunicationChannel {
address: String,
port: u16,
}
impl Drop for CommunicationChannel {
fn drop(&mut self) {
println!("Closing port {}:{}",
self.address, self.port);
}
}

impl CommunicationChannel {
fn create(address: &str, port: u16)
-> CommunicationChannel
{
println!("Opening port {}:{}", address, port);
CommunicationChannel {
address: address.to_string(),
port: port,
}
}
fn send(&self, msg: &str) {
println!("Sent to {}:{} the message '{}'",
self.address, self.port, msg);
}
}
let channel = CommunicationChannel::create(
"usb4", 879);
channel.send("Message 1");
{
let channel = CommunicationChannel::create(
"eth1", 12000);
channel.send("Message 2");
}
channel.send("Message 3");

该程序将打印:

Opening port usb4:879
Sent to usb4:879 the message 'Message 1'
Opening port eth1:12000
Sent to eth1:12000 the message 'Message 2'
Closing port eth1:12000
Sent to usb4:879 the message 'Message 3'
Closing port usb4:879

第二个语句为新声明的类型CommunicationChannel实现特征Drop。 由语言定义的这种特性具有特殊的属性,即它唯一的方法,名为drop,在对象被释放时自动被调用,因此它是一个“析构函数”。 通常,要为类型创建析构函数,只需为这种类型实现Drop特征即可。 与您的程序中未定义的任何其他特征一样,您无法为在程序外部定义的类型实现它。

第三个语句是一个块,它为我们的struct定义了两个方法:create constructor和send操作。

最后,有应用程序代码。 创建通信通道,并且这样的创建打印第一行输出。 发送一条消息,该操作打印第二行。 然后有一个嵌套块,其中创建另一个通信通道,打印第三行,并通过这样的通道发送消息,打印第四行。

嵌套块中创建的通道与具有相同现有变量名称的变量相关联,这会导致此变量影响另一个变量。 我们也可以为第二个变量使用不同的名称。

到目前为止,没有什么新东西。 但现在,嵌套块结束了。 这会导致内部变量被破坏,因此调用其drop方法,并打印第五行。

现在,在嵌套块结束后,第一个变量再次可见。 另一条消息发送给它,导致打印最后一行。 最后,第一个变量被销毁,并打印出最后一行。

在Rust中,语言和标准库已经释放了内存,因此不需要调用类似于C语言的自由函数或C ++语言的delete运算符的函数。 但其他资源不会自动释放。 因此,析构函数对于释放文件句柄,通信句柄,GUI窗口,图形资源和同步原语等资源非常有用。 如果使用库来处理这些资源,那么该库应该已经包含适用于处理资源的任何类型的Drop的实现。

析构函数的另一个用途是更好地理解如何管理内存。

struct S ( i32 );
impl Drop for S {
fn drop(&mut self) {
println!("Dropped {}", self.0);
}
}

let _a = S (1);
let _b = S (2);
let _c = S (3);
{
let _d = S (4);
let _e = S (5);
let _f = S (6);
println!("INNER");
}
println!("OUTER");

这将打印:

INNER
Dropped 6
Dropped 5
Dropped 4
OUTER
Dropped 3
Dropped 2
Dropped 1

请注意,对象的排序顺序与它们的构造顺序完全相反,并且只是当它们超出其范围时。

struct S ( i32 );
impl Drop for S {
fn drop(&mut self) {
println!("Dropped {}", self.0);
}
}
let _ = S (1);
let _ = S (2);
let _ = S (3);
{
let _ = S (4);
let _ = S (5);

let _ = S (6);
println!("INNER");
}
println!("OUTER");

这将打印:

Dropped 1
Dropped 2
Dropped 3
Dropped 4
Dropped 5
Dropped 6
INNER
OUTER

在这个程序中,没有变量,只有可变占位符,所以所有对象都是临时的。 临时对象在其语句结束时被销毁,即遇到分号时。 该计划相当于以下一项:

struct S ( i32 );
impl Drop for S {
fn drop(&mut self) {
println!("Dropped {}", self.0);
}
}
S (1);
S (2);
S (3);
{
S (4);
S (5);
S (6);
println!("INNER");
}
println!("OUTER");

赋值语义

以下程序有什么作用?

let v1 = vec![11, 22, 33];
let v2 = v1;

从概念上讲,首先,在堆栈中分配v1的头。 然后,当这样的向量具有内容时,在堆中分配用于这种内容的缓冲区,并将值复制到其上。 然后初始化头,以便它引用新分配的堆缓冲区。

然后在堆栈中分配v2的头部。 然后,使用v1初始化v2。 但是,这是如何实施的? 通常,至少有三种方法来实现此类操作:

•分享语义。 v1的标题被复制到v2的标题上,没有其他任何事情发生。随后,可以使用v1和v2,它们都引用相同的堆缓冲区;因此,它们指的是相同的内容,而不是两个相同但不同的内容。这种语义由垃圾收集语言实现,如Java。

•复制语义。分配了另一个堆buff。它与v1使用的堆缓冲区一样大,并且预先存在的缓冲区的内容被复制到新缓冲区。初始化v2的头部,以便它引用新分配的buf er。因此,这两个变量指的是最初具有相同内容的两个不同的缓冲区。默认情况下,Tis由C ++实现。

•移动语义。 v1的标题被复制到v2的标题上,没有其他任何事情发生。随后,可以使用v2,它指的是为v1分配的堆缓冲区,但不能再使用v1。默认情况下,这是由Rust实现的。

let v1 = vec![11, 22, 33];
let v2 = v1;
print!("{}", v1.len());

此代码生成在最后一行使用移动值:v1的编译错误。 当v1的值分配给v2时,变量v1不再存在。 编译器不允许尝试使用它,甚至只是为了获得它的长度。

让我们看看为什么Rust没有实现共享语义。 首先,如果变量是可变的,那么这种语义会有些混乱。 使用共享语义,在通过变量更改项目之后,当通过其他变量访问该项目时,该项目似乎也会更改。 它不直观,可能是错误的来源。 因此,只有只读数据才能接受共享语义。

但是在解除分配方面存在一个更大的问题。 如果使用了共享语义,则v1和v2都将拥有单个数据缓冲区,因此当它们被解除分配时,相同的堆缓冲区将被释放两次。 缓冲区不能分配两次而不会导致内存损坏,从而导致程序故障。 要解决此问题,使用共享语义的语言不会使用此类内存在变量范围的末尾释放内存,而是使用垃圾回收。

相反,复制语义和移动语义都是正确的。 实际上,关于释放的Rust规则是任何对象必须只有一个所有者。 当使用复制语义时,原始向量缓冲区保持其单个所有者,即v1引用的向量头,并且新创建的向量缓冲区获得其单个所有者,即v2引用的向量头。 另一方面,当使用移动语义时,单个向量缓冲区更改所有者:在赋值之前,其所有者是v1引用的向量头,并且在赋值之后,其所有者是v2引用的向量头。 在分配之前,v2标头尚不存在,并且在分配之后,v2标头不再存在。

为什么Rust没有实现复制语义?
实际上,在某些情况下,复制语义更合适,但在其他情况下,移动语义更合适。 自2011年以来,即使是C ++,也允许复制语义和移动语义。

#include <iostream>
#include <vector>
int main() {
auto v1 = std::vector<int> { 11, 22, 33 };
const auto v2 = v1;
const auto v3 = move(v1);
std::cout << v1.size() << " "
<< v2.size() << " " << v3.size();
}

此C ++程序将打印:0 3 3.向量v1首先复制到向量v2,然后移动到向量v3。 C ++移动标准函数清空向量但不会使其未定义。 因此,最后,v2有三个项目的副本,v3只有为v1创建的原始三个项目,v1为空。 Rust还允许复制语义和移动语义。

let v1 = vec![11, 22, 33];
let v2 = v1.clone();
let v3 = v1;
// ILLEGAL: print!("{} ", v1.len());
print!("{} {}", v2.len(), v3.len());

这将打印3 3。

这个Rust程序类似于上面的C ++程序,但是这里禁止访问v1,在最后一行但是因为它被移动了。 在C ++中,默认语义是一个副本,需要调用“move”标准函数来进行移动,在Rust中默认语义是一个移动,需要调用“clone”标准函数来制作 复印件。

此外,尽管在C ++中v1移动的向量仍然可以访问,但是在Rust中,这样的变量根本不可访问。

复制与移动性能

选择Rust来支持移动语义是关于性能的。 对于拥有堆缓冲区的对象(如向量),移动它比复制它更快,因为向量的移动只是标头的副本,而向量的副本需要分配和初始化 大堆缓冲区,最终将被解除分配。 一般来说,Rust的设计选择是允许任何操作,但是使用较小的语法来实现最安全和最有效的操作。

此外,在C ++中,移动的对象不再被使用,但是,为了使语言与遗留代码库向后兼容,移动的对象仍然可访问,并且程序员有可能错误地使用这些对象。 另外,清空移动的矢量具有(小)成本,并且当矢量被破坏时,应该检查它是否为空,并且还具有(小)成本。 Rust被设计为避免使用移动的对象,因此不会错误地使用移动的向量,并且编译器可以生成更好的代码,因为它知道向量何时被移动。

我们可以使用以下代码来衡量这种性能影响,这不是那么简单,因为否则编译器优化器会从循环中删除任何工作。

以下Rust程序使用复制语义。

use std::time::Instant;
fn elapsed_ms(t1: Instant, t2: Instant) -> f64 {
let t = t2 - t1;
t.as_secs() as f64 * 1000. + t.subsec_nanos() as f64 / 1e6
}
const N_ITER: usize = 100_000_000;
let start_time = Instant::now();
for i in 0..N_ITER {
let v1 = vec![11, 22];
let mut v2 = v1.clone(); // Copy semantics is used
v2.push(i);
if v2[1] + v2[2] == v2[0] {
print!("Error");
}
}
let finish_time = Instant::now();
print!("{} ns per iteration\n",
elapsed_ms(start_time, finish_time) * 1e6 / N_ITER as f64);

以下是与之相当的C ++程序。

#include <iostream>
#include <vector>
#include <ctime>
int main() {
const int n_iter = 100000000;
auto start_time = clock();
for (int i = 0; i < n_iter; ++i) {
auto v1 = std::vector<int> { 11, 22 };
auto v2 = v1; // Copy semantics is used
v2.push_back(i);
if (v2[1] + v2[2] == v2[0]) { std::cout << "Error"; }
}

auto finish_time = clock();
std::cout << (finish_time - start_time) * 1.e9
/ CLOCKS_PER_SEC / n_iter << " ns per iteration\n";
}

以下Rust程序使用移动语义。 它与前一个Rust程序的区别仅在于循环的第二行。

use std::time::Instant;
fn elapsed_ms(t1: Instant, t2: Instant) -> f64 {
let t = t2 - t1;
t.as_secs() as f64 * 1000. + t.subsec_nanos() as f64 / 1e6
}
const N_ITER: usize = 100_000_000;
let start_time = Instant::now();
for i in 0..N_ITER {
let v1 = vec![11, 22];
let mut v2 = v1; // Move semantics is used
v2.push(i);
if v2[1] + v2[2] == v2[0] {
print!("Error");
}
}
let finish_time = Instant::now();
print!("{} ns per iteration\n",
elapsed_ms(start_time, finish_time) * 1e6 / N_ITER as f64);

以下是与之相当的C ++程序。

#include <iostream>
#include <vector>
#include <ctime>
int main() {
const int n_iter = 100000000;
auto start_time = clock();
for (int i = 0; i < n_iter; ++i) {
auto v1 = std::vector<int> { 11, 22 };
auto v2 = move(v1); // Move semantics is used

v2.push_back(i);
if (v2[1] + v2[2] == v2[0]) { std::cout << "Error"; }
}
auto finish_time = clock();
std::cout << (finish_time - start_time) * 1.e9
/ CLOCKS_PER_SEC / n_iter << " ns per iteration\n";
}

以下是一对编译器在特定计算机中获得的大致时间,并启用了优化:

在Rust和C ++中,移动语义比复制语义更快。 顺便说一下,当使用移动语义时,这两种语言具有相同的性能,而当使用复制语义时,C ++比Rust好得多。 如果移动或复制的对象是一个大向量,或者是一个链接的对象树,而不是一个小向量,移动和复制之间的差异会大得多。

移动和销毁对象

所有这些概念不仅适用于向量,还适用于任何引用堆缓冲区的对象,如String或Box。 这个Rust程序:

let s1 = "abcd".to_string();
let s2 = s1.clone();
let s3 = s1;
// ILLEGAL: print!("{} ", s1.len());
print!("{} {}", s2.len(), s3.len());

这是一个类似的C ++程序:

#include <iostream>
#include <string>
int main() {
auto s1 = std::string { "abcd" };
const auto s2 = s1;
const auto s3 = move(s1);
std::cout << s1.size() << " "
<< s2.size() << " " << s3.size();
}

Rust程序将打印4 4,并且在程序结束时访问s1的任何尝试都将导致编译错误。 C ++程序将打印0 4 4,因为移动的字符串s1变为空。
而这个Rust程序:

let i1 = Box::new(12345i16);
let i2 = i1.clone();
let i3 = i1;
// ILLEGAL: print!("{} ", i1);
print!("{} {}", i2, i3);

类似于这个C ++程序:

#include <iostream>
#include <memory>
int main() {
auto i1 = std::unique_ptr<short> {
new short(12345)
};
const auto i2 = std::unique_ptr<short> {
new short(*i1)
};
const auto i3 = move(i1);
std::cout << (bool)i1 << " " << (bool)i2 << " "
<< (bool)i3 << " " << *i2 << " " << *i3;
}

Rust程序将打印12345 12345,任何在程序结束时访问i1的尝试都将导致编译错误。 C ++程序将打印0 1 1 12345 12345.在最后一个语句中,首先检查哪些唯一指针为空; 只有i1为null,因为它被移动到了i3。 然后,打印i2和i3引用的值。 仅当对象用于初始化变量时,以及在分配已具有值的变量时,对象才会移动,如下所示:

let v1 = vec![false; 3];
let mut v2 = vec![false; 2];
v2 = v1;
v1;

以及将值传递给函数参数时,如下所示:

fn f(v2: Vec<bool>) {}
let v1 = vec![false; 3];
f(v1);
v1;

并且当此时分配的对象没有引用实际堆时,例如 这个:

let v1 = vec![false; 0];
let mut v2 = vec![false; 0];
v2 = v1;
v1;

编译前三个程序中的任何一个,最后一个语句导致使用移动值编译错误。

特别是,在最后一个程序中,v1被移动到v2,即使它们都是空的,因此不使用堆。 为什么? 因为移动规则是由编译器应用的,所以它必须独立于运行时对象的实际内容。

但是,编译以下程序会导致最后一行出错。 怎么来的?

struct S {}
let s1 = S {};
let s2 = s1;
s1;

在这里编译器可以确保这些对象不包含对堆的引用,但它仍然抱怨移动。 为什么Rust不会对这种永远不会引用堆的类型使用复制语义?

这是理由。 用户定义的类型S现在没有对内存的引用,但是在将来维护软件之后,可以容易地添加对堆的一个引用,作为S的字段或作为S的字段的字段,等等。 因此,如果我们现在为S实现复制语义,当更改程序源以便直接或间接地向S添加String或Box或集合时,这种语义变化会导致很多错误。 所以,作为一项规则,最好保持移动语义。

需要复制语义

因此,我们已经看到,对于许多类型的对象,包括向量,动态字符串,框和结构,使用移动语义。 但是,以下程序是有效的。

let i1 = 123;
let _i2 = i1;
let s1 = "abc";
let _s2 = s1;
let r1 = &i1;
let _r2 = r1;
print!("{} {} {}", i1, s1, r1);

它将打印:“123 abc 123”。 怎么来的?

好吧,对于原始数字,静态字符串和引用,Rust不使用移动语义。 对于这些数据类型,Rust使用复制语义。

为什么? 我们之前看到,如果一个对象可以拥有一个或多个堆对象,那么它的类型应该实现移动语义; 但如果它不能拥有任何堆内存,它也可以实现复制语义。 移动语义对于原始类型来说是一种麻烦,并且它们不可能被改变为拥有一些堆对象。 因此,对于他们来说,复制语义是安全,有效和方便的。

因此,一些Rust类型实现了复制语义,而其他类型实现了移动语义。 特别是,数字,布尔值,静态字符串,数组,元组和对任何类型的引用都实现了复制语义。 相反,动态字符串,框,任何集合(包括向量),枚举,结构和元组结构默认实现移动语义。

克隆对象

但是,关于对象的复制,还有另一个重要的区别。 实现复制语义的所有类型都可以很容易地复制,具有赋值; 但是,使用克隆标准函数也可以复制实现移动语义的对象。 我们已经看到克隆函数可以应用于动态字符串,框和向量。 但是,对于某些类型,克隆函数不适用,因为不适合任何类型的复制。 考虑文件句柄,GUI窗口句柄或互斥锁句柄。 如果复制其中一个,然后销毁其中一个副本,则会释放底层资源,并且句柄的其他副本具有不一致的句柄。 因此,关于复制的能力,有三种对象:

•没有任何东西的对象,复制起来既简单又便宜。

•拥有某些堆对象但不拥有外部资源的对象,因此可以复制,但具有显着的运行时成本。

•拥有外部资源的对象,如文件句柄或GUI窗口句柄,因此永远不应复制它们。

第一类对象的类型可以实现复制语义,它们应该是,因为它更方便。我们称它们为“可复制对象”。

第二类对象的类型可以实现复制语义,但是它们应该实现移动语义,以避免不需要的重复的运行时成本。而且,他们应该提供一种明确复制它们的方法。我们称它们为“可克隆但不可复制的对象”。

第三类对象的类型也应该实现移动语义,但是它们不应该提供显式复制它们的方法,因为它们拥有Rust代码不能复制的资源,并且这样的资源应该只有一个所有者。让我们称它们为“不可克隆的对象”。

当然,也可以显式复制任何可以自动复制的对象,因此任何可复制对象也是可复制对象。

总而言之,某些对象是不可复制的(如文件句柄),而其他对象是可复制的(显式)。一些可复制对象也(隐式)可复制(如数字),而其他可复制对象(如集合)。

为了区分这三个类别,Rust标准库包含两个特定的特征:复制和克隆。 任何实现复制特征的类型都是可复制的; 任何实现克隆特征的类型都是可克隆的。 因此,上述三种特征以这种方式表征:

•实现复制和克隆的对象(如原始数字)是“可复制的”(也可“复制”)。 它们实现了复制语义,也可以显式克隆它们。
•实现Clone但未实现Copy的Te对象(如集合)是“可复制但不可复制的”。 Tey实现移动语义,但可以明确克隆它们。

•Te文件对象(如文件句柄)既不实现复制也不实现克隆,是“不可复制的”(也是“不可复制的”)。 Tey实现移动语义,它们无法克隆。

•没有对象可以实现复制但不能实现克隆。 这意味着没有对象是“可复制但不可复制”的,因为这样的对象将被隐式复制但不能明确复制,这是毫无意义的。 以下是所有这些案例的示例:

let a1 = 123;
let b1 = a1.clone();
let c1 = b1;
print!("{} {} {}", a1, b1, c1);
let a2 = Vec::<bool>::new();
let b2 = a2.clone();
let c2 = b2;
print!(" {:?}", a2);
// ILLEGAL: print!("{:?}", b2);
print!(" {:?}", c2);
let a3 = std::fs::File::open(".").unwrap();
// ILLEGAL: let b3 = a3.clone();
let c3 = a3;
// ILLEGAL: print!("{:?}", a3);
print!(" {:?}", c3);

该程序打印:“123 123 123 [] []文件”,然后打印有关当前目录的一些信息。它只能编译,因为已经注释掉了三个非法语句。

首先,a1被声明为原始数字。这种类型是可复制的,因此可以将它们显式克隆到b1并隐式复制到c1。因此,有三个不同的对象具有相同的值,我们可以打印它们。然后,a2被声明为集合,特别是布尔值的向量。这种类型是可复制的但不可复制,因此它可以被明确地克隆到b2,但是b2到c2的赋值是一个移动,它使b2保持未定义,因此,在该赋值之后,我们可以打印a2和c2,但是尝试编译打印b2的语句将生成错误消息:使用移动值:b2

最后,a3被声明为资源句柄,特别是文件句柄。这种类型不可复制,因此尝试编译克隆a3的语句会生成错误消息:在当前作用域中找不到名为clone的类型为std :: fs :: File的方法。允许将a3分配给c3,但它是一个移动,因此我们可以打印a3的一些调试信息,但是尝试编译打印a3的语句会产生错误消息:使用移动值:a3

使类型可克隆或可复制

如前所述,默认情况下,枚举,结构和元组结构不实现复制特征或克隆特征,因此它们是不可克隆的。 但是,您可以为克隆特征和复制特征实现每个克隆特征的单个克隆特征。

以下程序是非法的:

struct S {}
let s = S {};
s.clone();

但它是否足以实现克隆,使其有效。

struct S {}
impl Clone for S {
fn clone(&self) -> Self { Self {} }
}
let s = S {};
s.clone();

请注意,要实现Clone,需要定义clone方法,该方法必须返回其类型必须等于其参数类型的值。 此值也应该等于其参数的值,但不会检查。

实现克隆不会自动实现复制,因此以下程序是非法的:

struct S {}
impl Clone for S {
fn clone(&self) -> Self { Self {} }
}
let s = S {};
s.clone();
let _s2 = s;
s;

但它是否足以实现复制,使其有效。

struct S {}
impl Clone for S {
fn clone(&self) -> Self { Self {} }
}
impl Copy for S {}
let s = S {};
s.clone();
let _s2 = s;
s;

请注意,Copy的实现可以为空; 它足以声明实现了Copy,以激活复制语义。 但以下程序是非法的:

struct S {}
impl Copy for S {}

错误消息解释了原因:“特征绑定main :: S:std :: clone :: Clone不满意”。 仅当还实现了克隆特征时,才能实现复制特征。
但是以下程序也是非法的:

struct S { x: Vec<i32> }
impl Copy for S {}
impl Clone for S {
fn clone(&self) -> Self { *self }
}

错误消息显示:“此类型可能无法实现特性”复制“,表示类型Vec 。

该程序尝试为包含向量的结构实现复制特征。 Rust允许您仅为仅包含可复制对象的类型实现复制特征,因为复制对象意味着复制其所有成员。 在这里,Vec没有实现Copy trait,因此S无法实现它。
相反,以下程序有效:

struct S { x: Vec<i32> }
impl Clone for S {
fn clone(&self) -> Self {
S { x: self.x.clone() }
}
}
let mut s1 = S { x: vec![12] };
let s2 = s1.clone();
s1.x[0] += 1;
print!("{} {}", s1.x[0], s2.x[0]);

它将打印:“13 12”。
这里,S结构不可复制,但它是可复制的,因为它实现了克隆特征。 因此,可以将s1的副本分配给s2。 之后,修改了s1,print语句显示它们不同。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/m0_37696990/article/details/82873275
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2023-01-03 14:46:43
  • 阅读 ( 409 )
  • 分类:Go Web框架

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢