Rust常用特型之Clone+Copy特型。在Rust标准库中,存在很多常用的工具类特型,它们能帮助我们写出更具有Rust风格的代码。
今天,我们把Clone 和 Copy 特型放在一起学习。
(注:本文更多的是对《Programing Rust 2nd Edition》的自己翻译和理解,并不是原创)
一 什么是Clone
标准库的Clone
特型意味着实现它的类型可以制作一份值的副本。它的定义如下:
trait Clone: Sized {fn clone(&self) -> Self;fn clone_from(&mut self, source: &Self) {*self = source.clone()}
}
它有两个函数,其中一个函数有默认实现,因此我们只需要自己实现第一个函数clone()
就好,实际上,运用的最多的也是clone()
函数。
clone()
函数会构造自身的独立备份并且返回它。因为该函数的返回类似是Self
, 因此函数不可能返回unsized
值。这是因为Clone
特型本身拓展了Sized
特型,因此,实现了Clone
特型的类型必须是Sized
。
克隆一个值通常需要为它所拥有的任何内容重新分配资源,所以克隆在时间和内存上都开销很大。例如, 克隆一个Vec<String>
不仅仅复制了向量本身,也复制了它的每一个字符串元素。这就是为什么Rust不自动实现克隆,而需要你手动显式调用的原因。引用计数指针类型例如Rc<T>
andArc<T>
是个例外,克隆他们只是简单的增加一个引用计数,并且返回给你一个新的指针。
clone_from
函数从一个源值复制,它的默认实现很简单,只是调用了source的clone
函数。因为函数参数self
的类型是&mut
,因此这里只需要重新赋值即可*self = value
。这样做是没有问题的,但是某些场景下,有更快的方法来达到同样的效果。
例如 假定s
和t
都是String
,语句s = t.clone()
它首先克隆t
,然后drop
掉s
的旧值,然后移动新复制的值到s
。这里涉及到堆的分配和释放两种操作。但是如果指向最开始s
的堆缓冲区有足够的容量来容纳t
的元素,那么我们就不需要重新分配或者释放堆。你只是简单复制t
的文本到s
的缓冲区并修改长度即可。
在通用代码中,您应该尽量使用clone_from
以利用可能存在的优化。这里我前面说错了,我们常用clone
函数,但是这里更推荐clone_from
函数,虽然它可能会多拼一点.这里的真实意思其实是这样的
我们首先看clone_from
的注释(vscode中的tooltips):
Performs copy-assignment from
source
.
a.clone_from(&b)
is equivalent toa = b.clone()
in functionality, but can be overridden to reuse the resources ofa
to avoid unnecessary allocations.
上面解释的很清楚了,这里再说一下,有下面两种情况:
-
s 之前未定义或者未被初始化过 ,此时
s
无法转换为self
,因为不存在或者未初始化,只能使用s = t.clone()
-
s 之前初始化过,需要重新赋值,这时可以使用
clone_from
以便可能进行优化。let mut s = "Hello World".to_owned(); let t = "Hello Hi".to_owned(); s.clone_from(&t); // Clone::clone_from(&mut s, &t); // 这种方式也是可行的 // String::clone_from(&mut s, &t); // 这种方式也是可行的 println!("s: {s}");
这里也可以看出特型的几种调用方式:
- 值调用
- 特型名称调用
- 类型名称调用
其中 2和3 相当于类的静态方法,1相当于类的实例上的方法
如果你的克隆实现只是简单的复制你的类型中的每个字段或者元素,并且默认的clone_from
也很适用,那么Rust可以自动帮你实现Clone
特型,你只需要在你的类型定义上面加上#[derive(Clone)]
就行。
在标准库中,几乎所有有确切复制含义的类型都实现了Clone
特型。例如像bool
和i32
这样的元数据类型实现了Clone
,像String
,Vec<T>
和HasMap
等容器类型也实现了Clone
。有些类型没有明确的复制含义,例如std::sync::Mutex
,它们没有实现Clone
特型。一些类型可以复制,但是复制可能失败(操作系统有可能无法拥有必需的资源),例如std::fs::File
类型,也没有实现Clone
,因为clone
函数是不能失败的。 作为替代方案,std::fs::File
提供了一个try_clone
函数,它返回一个Result<File>
,它可以用来处理失败情况。
二 Copy
Clone是复制,Copy也是复制,那么很多人肯定和我一样有疑惑,这两者有什么区别呢?不急,我们先看书中怎么说的。
对于绝大多数类型来说,赋值是move
值,而不是复制他们。移动一个值使得追踪他们拥有的资源更简单。但是也有例外,不拥有任何资源的类型可以是Copy
类型,此时赋值仅是对源值的复制,并不会移动源值并让原变量变成未初始化状态。
Copy
类型是指实现了std::marker::Copy
特型的类型,该特型定义如下:
trait Copy: Clone { }
可以看出,定义非常简单,Copy
特型只是简单扩展了一下Clone
,并没有定义新的方法。也就是说,Copy
类型一定是Clone
的,而Clone
类型却未必是Copy
的。例如String
,它是Clone
的,但是不是Copy
的。 u32
类型,上面提到过,它是Clone
的,同时也是Copy
的。
那么Copy
类型为什么也要是Clone
类型呢?因为复制行为必须依赖Clone
实现。
在Rust中,这种简单特型扩展的用法很常见,你可以利用它从一类类型中(Clone类型)再划出一个小类型(Copy类型)。
如果想对自己的类型实现Copy
特型,只需要这么做
impl Copy for MyType { }
因为Copy
特型是一个标记类型,对Rust语言来说有特殊的含义,因此,Rust中Copy
类型的复制行为只能是字节到字节的复制。如果类型拥有其它资源,例如堆缓冲区和操作系统句柄,他们是不能实现Copy
类型的。
(那么这个约束是怎么强制的?应该是编译器检查的)
在学习Drop
特型的时候提到,任何实现了Drop
特型的类型不能再是Copy
类型,Rust会假定如果一个类型需要特殊的清理逻辑,那么它必定需要特殊的复制机制,因此不能实现Copy
。
和Clone
一样,你可以让Rust自动为你复现Copy
特型,方式为使用#[derive(Copy)]
语法。你也会经常看到一次性的derive
两个特型:#[derive(Copy, Clone)]
。这两种方式的区别是,如果使用方式一,那么先必须为该类型实现了Clone
特型,如果使用方式二,则一次性完整,大多数情况下使用方式二。
// 方式一
#[derive(Copy)]
struct MyType;impl Clone for MyType {fn clone(&self) -> Self {Self { }}
}// 方式二#[derive(Clone, Copy)]
struct MyType;
记住,你不能为String
实现Copy
特型,因为两者都是外部的。一个结构体,只有所有字段是Copy
特型时才能是可Copy
的,例如
#[derive(Clone, Copy,Debug)]
struct MyType {name:String
}
这段代码会提示你该类型无法复制Copy
。我们把name
字段换一下就可以了,如下:
#[derive(Clone, Copy,Debug)]
struct MyType {age:u32
}
对于上面的MyType
,我们运行如下代码片断:
let a = MyType {age:10};
let b = a;
println!("{a:?},{b:?}");
会输出MyType { age: 10 },MyType { age: 10 }
。 这里可见发生了Copy
而不是Move
。
注意:
谨慎考虑将一个类型设计为Copy
类型,虽然这么做使用该类型会比较简单,但是对它的实现有比较大的限制。并且,复制是比较昂贵的。