复制值,第二部分
让我们考虑与之前相同的例子,但稍作调整:使用u32
代替String
作为类型。
#![allow(unused)] fn main() { fn consumer(s: u32) { /* ... */ }fn example() { let s: u32 = 5; consumer(s); let t = s + 1; // 无错误!}} }
这将无误编译!这是怎么回事?String
和u32
之间的区别是什么,使得后者无需.clone()
就能工作?
Copy
Copy
是Rust标准库中定义的另一个特性:
#![allow(unused)] fn main() { pub trait Copy: Clone { } }
它是一个标记特性,类似于Sized
。
如果一个类型实现了Copy
,创建该类型的实例时就不需要显式调用.clone()
:Rust会隐式地为你处理。u32
就是一个实现Copy
的类型示例,因此上述代码能无误编译:当调用consumer(s)
时,Rust通过对s
进行位级复制来创建一个新的u32
实例,然后将这个新实例传递给consumer
。这一切都在幕后自动完成,无需你的介入。
什么可以是Copy
?
Copy
并不等同于“自动克隆”,尽管它暗示了这一点。类型必须满足一些条件才能被允许实现Copy
。
首先,它必须实现Clone
,因为Copy
是Clone
的子特性。
这是有道理的:如果Rust能够_隐式_创建类型的实例,那么通过调用.clone()
也应该能够_显式_创建新实例。
但这还不是全部。还需满足几个其他条件:
- 类型不管理任何额外资源(如堆内存、文件句柄等),除了它在内存中占用的
std::mem::size_of
字节。 - 类型不是可变引用(
&mut T
)。
如果这两个条件都满足,那么Rust就可以通过执行原实例的位级复制安全地创建一个新实例——这常被称为memcpy
操作,源自C标准库中执行位级复制的函数。
案例研究1:String
String
是一个不实现Copy
的类型。
为什么?因为它管理着额外的资源:用于存储字符串数据的堆分配内存缓冲区。
假设Rust允许String
实现Copy
。
那么,当通过位级复制原始实例创建新的String
实例时,原始实例和新实例都将指向同一内存缓冲区:
s copied_s
+---------+--------+----------+ +---------+--------+----------+
| pointer | length | capacity | | pointer | length | capacity |
| | | 5 | 5 | | | | 5 | 5 |
+--|------+--------+----------+ +--|------+--------+----------+
| |
| |
v |
+---+---+---+---+---+ |
| H | e | l | l | o | |
+---+---+---+---+---+ |
^ |
| |
+------------------------------------+
这很糟糕!
两个String
实例都会在超出作用域时尝试释放内存缓冲区,导致重复释放错误。
你也可能创建两个指向同一内存缓冲区的不同可变&mut String
引用,违反了Rust的借用规则。
案例研究2:u32
u32
实现了Copy
。实际上,所有整数类型都是如此。
一个整数就是内存中代表数字的那些字节。没有别的!
如果复制那些字节,就会得到另一个完全有效的整数实例。
没有任何不良后果,所以Rust允许这样做。
案例研究3:&mut u32
当我们介绍所有权和可变借用时,明确了一条规则:任何时候对一个值只能有一个可变借用。
这就是&mut u32
不实现Copy
的原因,即便u32
本身实现了。
如果&mut u32
实现了Copy
,你就可以创建多个指向同一值的可变引用,并同时在多处修改它。
这将违反Rust的借用规则!因此,无论T
是什么,&mut T
都不实现Copy
。
实现Copy
大多数情况下,你不需要手动实现Copy
。
你可以这样派生它:
#![allow(unused)] fn main() { #[derive(Copy, Clone)] struct MyStruct { field: u32, } }
参考资料
- 本节的练习位于
exercises/04_traits/11_copy