掌握 Rust 所有权并没有那么难,只要理解一些简单的概念和设计取舍,就足够了。

参考了认知所有权

配合 Rust Playground 使用更佳!

# Copy, Move 和 Clone

# 等号表示 Copy(值拷贝) 或 Move(所有权转移)

如果被赋值的类型实现了 Copy trait,那么等号表示值拷贝,否则表示所有权的转移

整型实现了 Copy trait,赋值时表示的就是 Copy(值拷贝)

fn main() {
    let a = 3;
    let b = a;
    println!("{}, {}", a, b);
}
// 2, 3

字符串没有实现 Copy trait,赋值时表示 Move(所有权转移)

fn main() {
    let a = String::from("abc");
    let b = a;
    println!("{}, {}", a, b);
}
/*
error[E0382]: borrow of moved value: `a`
 --> src/main.rs:4:20
  |
2 | let a = String::from("abc");
  |     - move occurs because `a` has type `std::string::String`, which does not implement the `Copy` trait
3 | let b = a;
  |         - value moved here
4 | println!("{}, {}", a, b);
  |                    ^ value borrowed here after move
*/

# Copy = Move + Clone

如果想对没有实现 Copy trait 的数据实现 Copy(值拷贝) 怎么办?

对于 String 和大多预定义的结构体,它们已经实现了 Clone trait,调用 clone 方法得到新的值

fn main() {
    let a = String::from("abc");
    let b = a.clone();
    println!("{}, {}", a, b);
}
// abc, abc

如果是自己能控制的数据结构,声明实现 Clone trait 即可

#[derive(Clone)]
struct Point {
    x: i64,
    y: i64,
}

fn main() {
   let p = Point { x: 1, y: 2 };
   let copy = p.clone();
   println!("{}, {}", p.x, copy.y);
}
// 1, 2

如果不能修改数据结构,怎么 clone 呢?只能手动了

struct Point {
    x: i64,
    y: i64,
}

fn main() {
   let p = Point { x: 1, y: 2 };
   let copy = Point{ x: p.x, y: p.y };
   println!("{}, {}", p.x, copy.y);
}
// 1, 2

# Move = Copy + ?

怎样对一个实现了 Copy trait 的变量执行 move(所有权转移) 语义的赋值?

很遗憾,没有找到一个可行的方法

copyable 的变量“滑不溜秋”,与所有函数调用释放内存的方法绝缘
其次并没有一个命令语法来完成所有权强制转移的操作

如果不想发生内存复制过程,可以直接使用引用

# 函数调用时的赋值

和所有编程语言一样,在函数调用传参时也会触发“赋值”过程,可能有 Copy(值拷贝)/Move(所有权转移) 发生

# 总结

  1. 实现了 Copy trait 的数据结构基本没有所有权问题,实际上也很难想象一个数字也有所有权是怎样的灾难
  2. 另一方面 Copyable 的变量所有权是无法被 move 的,但是非 copyable 的变量可以配合 Clone trait 来自由控制
  3. 要注意如果没有实现 Clone,一些特定场景的使用会变得非常麻烦

# 引用

# 引用是为了简化所有权回传

上面说了在调用函数时传参相当于复制,可能会发生 Move
为了把所有权拿回来,需要把数据再作为返回值回传

fn do_something(s: String) -> String {
    println!("Do something!");
    s
}

fn main() {
    let a = String::from("abc");
    let b = do_something(a);  // 两次 Move
    println!("b: {}", b);
}
/*
Do something!
b: abc
*/

使用引用,把所有权“借”出去就可以简化写法了

fn do_something(_s: &String) {
    println!("Do something!");
}

fn main() {
    let a = String::from("abc");
    do_something(&a);  // 一次引用
    println!("a: {}", a);
}
/*
Do something!
a: abc
*/

# 可以同时存在多个不可变引用

fn do_something(_s: &String) {
    println!("Do something!");
}

fn main() {
    let a = String::from("abc");
    let b = &a;
    do_something(&a);
    println!("a: {}, b: {}", a, b);
}
/*
Do something!
a: abc, b: abc
*/

# 可变引用不能与其他引用共存

如果有一个可变引用,就不能有其他的可变引用或者不可变引用了

fn main() {
    let mut a = String::from("abc");
    let b = &mut a;
    let c = &a;
    println!("b: {}, c: {}", b, c);
}
/*
error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
 --> src/main.rs:4:13
  |
3 |     let b = &mut a;
  |             ------ mutable borrow occurs here
4 |     let c = &a;
  |             ^^ immutable borrow occurs here
5 |     println!("b: {}, c: {}", b, c);
  |                              - mutable borrow later used here
*/

这里可以将不可变引用理解为“读引用”,将可变引用理解为“写引用”
这样的读写规则能解决数据竞争(data race)的问题:随便读,但是要写的时候,不能有其他读写的途径

# “借”出去还没还的能 Move 吗?

不可以!

fn main() {
    let a = String::from("abc");
    let b = &a;
    let c = a;
    println!("b: {}, c: {}", b, c);
}
/*
error[E0505]: cannot move out of `a` because it is borrowed
 --> src/main.rs:4:13
  |
3 |     let b = &a;
  |             -- borrow of `a` occurs here
4 |     let c = a;
  |             ^ move out of `a` occurs here
5 |     println!("b: {}, c: {}", b, c);
  |                              - borrow later used here
*/

# 快点还!

实际上归还已经是很智能的了,一般语言到大括号结束才有所动作,而在 rust 中,变量的最后一次使用位置视为结束销毁位置

fn main() {
    let mut a = String::from("abc");
    let b = &mut a;
    let c = &a;
    // println!("b: {}, c: {}", b, c);
    println!("Success!");
}
// Success!

不过这种操作还是有点被动,而且很可能因为代码的变化而变得复杂,可以顺手给可变引用加个作用域

fn main() {
    let mut a = String::from("abc");
    {
        let b = &mut a;
        println!("b: {}", b);
    }
    let c = &a;
    println!("c: {}", c);
}
/*
b: abc
c: abc
*/