移动与拷贝
在 Rust 中,如果一个类型实现了 Copy trait
,那么该类型的值如果发生资源转移,默认会进行拷贝。例如常用的数字类型(u8,u16等), 只读引用等都实现了该 trait,例如:
fn main() {
let x = 1;
let y = x; // copy here
println!("{} {}", x, y); // x 仍然可以访问
let a = "hello";
let b = a; // copy here, 注意仅仅是 copy 引用,而不是字符串
println!("{} {}", a, b); // a 任然可以访问
let c = String::from("hello"); // - move occurs because `c` has type `String`, which does not implement the `Copy` trait
let d = c; // - value moved here
println!("{} {}", c, d); //error! value borrowed here after move,无法访问 c,因为所有权已经转移给 d。
}
由于 String 并未实现 copy trait,因此在赋值时默认行为为 move 而不是 copy,println 已经无法访问失去所有权的 c。
移动发生时机
移动并不是只有赋值时才会发生,还包括传递参数,模式匹配以及闭包:
struct People {
sex: String,
name: String,
}
fn print_people(p: People) {
println!("{:?}", p)
}
fn main() {
// 传递参数时移动
let p = People {
name: String::from("hello"),
sex: String::from("b"),
};
print_people(p); // 资源移动
// 模式匹配时移动
let p = People {
name: String::from("hello"),
sex: String::from("b"),
};
match p {
People { sex, name } => println!("{}", name), // 资源移动
_ => (),
}
let x = String::from("hello");
{
let y = ||x; // 被 lambda 表达式捕获,资源移动
}
println!("{}", x); // 无法在此处使用
}
对于结构体来说,甚至调用方法也可能导致该结构体本身被 move:
struct MyMove;
impl MyMove {
fn moved(self) -> Self{
return self; // 通过返回值移动了资源
}
}
fn main() {
let me = MyMove;
me.moved();
let me2 = me; // 无法访问被移动的资源
}
另一个常见的例子为使用 into_iter
会移动数组本身。
fn main() {
let v = vec![String::from("1"), String::from("2")];
for i in v.into_iter(){ // `v` moved due to this method call
println!("{}", i);
}
let v1 = v; // value used here after move
}
部分移动
对于复杂类型,例如结构体来说,资源移动可能只发生在某一个部分,而不是整体,例如:
fn main() {
let p = People{
name: "linex".to_string(),
sex: "g".to_string()
};
let name = p.name; // 仅移动 name
println!("{} {}", name, p.sex); // p.sex 仍然可以访问
let p1 = p; // value used here after partial move,但是 p 已经不可访问了
}
需要注意的是,部分移动对 Vec
类型并不生效。
引用
在不需要转移资源时可以使用引用,引用并不持有资源:
fn main() {
let v = vec![String::from("a"), String::from("b")];
let c = &v;
let d = &v;
println!("{:?} {:?} {:?}", v, c, d); // a, b, c都可以继续访问
}
引用默认为只读的,不可以修改资源,除非显示声明为可变引用:
fn main() {
let mut v = vec![String::from("a"), String::from("b")];
let c = &mut v;
c.push("c".to_string()); // 可变引用,可以修改资源
println!("{:?}", v); // ["a", "b", "c"]
}
需要注意的是,在 Rust 中:
- 可以有多个只读引用
- 只可以有一个可变引用
- 可变引用与只读引用不能同时存在
- 可变引用未实现 Copy trait(只能存在一个可变引用,所以不能 copy)
关于第 4 点例子如下:
fn main() {
let mut v = vec![String::from("a"), String::from("b")];
let c = &mut v;
c.push("c".to_string());
let d = c;
println!("{:?}", c); // value borrowed here after move
}
由于引用并不真正的持有资源。因为无论是可变或者只读引用,都无法对资源进行部分移动。
#[derive(Debug, Clone)]
struct People {
sex: String,
name: String,
}
fn main() {
let mut p = People{
name: "linex".to_string(),
sex: "g".to_string()
};
let p1 = &mut p;
let name = p1.name; // cannot move out of `p1.name` which is behind a mutable reference
let name_ref = &p1.name; // 但是仍然可以访问 p1.name 的引用
}
由于引用无法进行部分移动,因此对引用使用模式匹配时,解构后得到的值也是引用:
fn main() {
let x = (1, 2);
let y = &x;
match y {
(v, u) => v // type v = &i32, u = &i32
}
}
注意,由于在 rust 临时资源会马上被销毁,因此注意不要使用临时资源的引用,例如:
fn say_hello(name: &str) -> String{
format!("hello {}", name)
}
fn main() {
let hello_lily = say_hello("lily").as_str();
println!("{}", hello_lily);
}
error[E0716]: temporary value dropped while borrowed
--> src/main.rs:6:22
|
6 | let hello_lily = say_hello("lily").as_str();
| ^^^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary which is freed while still in use
7 | println!("{}", hello_lily);
| ---------- borrow later used here
|
= note: consider using a `let` binding to create a longer lived value
say_hello 返回的为临时资源,会直接销毁,因此无法转为引用。需要先绑定到一个变量:
fn say_hello(name: &str) -> String{
format!("hello {}", name)
}
fn main() {
let hello_lily = say_hello("lily");
let hello_lily_str = hello_lily.as_str();
println!("{}", hello_lily_str); // hello lily
}
生命周期标注
当使用引用时可以避免资源移动,但是也带来了新的问题。例如,当某个方法返回了一个应用,该应用可能有两种来源:
- 来自于方法内部创建的资源,实际上这在 rust 中是不可能的,因为 Rust 会检测悬垂引用并且抛出编译异常。
- 来源于静态生命周期的应用。
- 来源于方法传入的引用,有可能。
第 3 种情况会相对复杂,例如编写一个返回长度更大的字符串的方法:
fn largest(s1: &String, s2: &String) -> &String{
if s1.len() > s2.len(){
s1
}else{
s2
}
}
fn main() {
let s1 = String::from("1234");
let s2 = String::from("567");
let s3 = largest(&s1, &s2);
}
看起来完美无缺的代码实际上无法通过编译。Rust 会提示需要一个显式的生命周期:
10 | fn largest(s1: &String, s2: &String) -> &String{
| ------- ------- ^ expected named lifetime parameter
这是为什么呢,考虑以下情况:
fn main() {
let s1 = String::from("1234");
let s3;
{
let s2 = String::from("56789");
s3 = largest(&s1, &s2); //s3 将会指向 s2
}// s2 将会在这里被销毁,因此 s3 此时为悬垂引用
println("{}", s3);
}
但是由于返回哪个字符串是运行时决定的,对于此种情况 rust 此时无法在编译期完成生命周期的检测,因此需要手动的标注生命周期:
fn largest<'a>(s1: &'a String, s2: &'a String) -> &'a String{
if s1.len() > s2.len(){
s1
}else{
s2
}
}
fn main() {
let s1 = String::from("1234");
let s2;
let s3;
{
s2 = String::from("567");
s3 = largest(&s1, &s2);
}
}
此时使用了 rust 中的生命周期标注语法 <'a>
,其中 'a
表示一个生命周期。而 s1: &'a String, s2: &'a String
则表示 s1, s2 都具有共同的生命周期。因此这里的 'a
表示 s1, s2 生命周期重叠的部分,或者说较短的一部分。 -> &'a String
则表示返回值的生命周期不能比 'a
更长。这是很好理解的,因为如果返回值的生命周期长于参数的生命周期,则可能出现悬垂引用。此时运行可以得到结果:
22 | s3 = largest(&s1, &s2);
| ^^^ borrowed value does not live long enough
23 | }
可以看出,在进行标注之后,rust 在能在编译器就检测到入参 s2 的生命周期相比返回值 s3 的生命周期更短,从而抛出异常。
可能有同学看到这里会问,为什么不默认认为返回值的生命周期必须短于入参呢?考虑以下情况:
fn which_one_largest(s1: &String, s2: &String) -> &'static str{
if(s1.len() > s2.len()){
"first"
}else{
"second"
}
}
此时,返回的 &str
是个一个字符串字面量,生命周期全局有效(标记为 'static),并不依赖于 s1 或者 s2 任意一个生命周期。因此,并不是所有情况返回值都依赖于入参,Rust 也无法帮我们做这种假设。
除方法以外,持有引用的结构体也需要进行生命周期标注,考虑以下情况:
struct TestValue<'a>{
r: &'a i32
}
fn main() {
let t ;
{
let x = 1;
t = TestValue{
r: &x // ^^ borrowed value does not live long enough
};
}
println!("{}", t);
}
从标注来看,&x
应该比 t
活的更久,但是明显 &x
的生命周期短于 t
,因此 Rust 会在编译期抛出异常。