Rust 所有权

279 阅读6分钟

所有权的移动

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}, world!", s1);
}

会出错:

error[E0382]: borrow of moved value: `s1`
 --> raii.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`

Rust 禁止你使用无效的引用

所有权的移动(Move) 有点像 4*100m 赛跑, 运动员有接力棒的时候,才能跑动,你跑完一段距离,交给了 B,B 才能继续跑,而你必须要停下了,因为接力棒不在你手里了。(这个规则跟真实世界不太一样,我们可以设定这样的一个规则,只有人手里有接力棒的时候,才能跑,一队也只能有一个接力棒)。

克隆

我们 确实 需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的通用函数。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {}, s2 = {}", s1, s2);
}

// s1 = hello, s2 = hello

但下面这段代码似乎与我们刚刚学到的内容相矛盾:没有调用 clone,不过 x 依然有效且没有被移动到 y 中。

fn main() {
    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);
}

// x = 5, y = 5

原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量y后使x无效。换句话说,这里没有深浅拷贝的区别,所以这里调用clone并不会与通常的浅拷贝有什么不同,我们可以不用管它。

说了这么多, x y是基本数据类型,是在栈上分配的,基本类型在编译时候就确定了内存大小。 所有权的移动是用在堆堆上分配内存, 堆内存动态增长,

所有权与函数

fn main() {
    let s = String::from("hello"); // s 进入作用域

    takes_ownership(s); // s 的值移动到函数里 ...

    // ... 所以到这里s不再有效

    let x = 5; // x 进入作用域

    makes_copy(x); // x 应该移动函数里,
    // 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作

fn takes_ownership(some_string: String) {
    // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) {
    // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

返回值与作用域

fn main() {
    let s1 = gives_ownership(); // gives_ownership 将返回值
    // 移给 s1

    let s2 = String::from("hello"); // s2 进入作用域

    let s3 = takes_and_gives_back(s2); // s2 被移动到
    // takes_and_gives_back 中,
    // 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {
    // gives_ownership 将返回值移动给
    // 调用它的函数

    let some_string = String::from("hello"); // some_string 进入作用域.

    some_string // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String {
    // a_string 进入作用域
    a_string // 返回 a_string 并移出给调用的函数
}

变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过drop被清理掉,除非数据被移动为另一个变量所有。

rust所有权的Move有点像接力棒

引用 references

fn main() {
    let s1 = String::from("hello");
    let len = calc_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calc_length(s: &String) -> usize {
    s.len()
}

// The length of 'hello' is 5

注意我们传递 &s1calc_length,同时在函数定义中,使用&String而不是 String。这些&符号就是引用,它们允许你使用值但不获取其所有权。

let s1 = String::from("hello");
let len = calc_length(&s1);

&s1 语法让我们创建一个指向值s1的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。

fn calc_length(s: &String) -> usize {
    s.len()
} 
// 这里s离开了作用域。但因为它并不拥有引用值的所有权, 所以什么也不会发生

我们将获取引用作为函数参数称为借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。

如果尝试修改借用的变量呢?

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(str: &String) {
    str.push_str(", world");
}

如果我们尝试修改借用的变量呢?正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。

可变引用

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(str: &mut String) {
    return str.push_str(", world");
}

不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s; // 出错

    println!("{}, {}", r1, r2);
}

错误如下:

error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> raii.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

error: aborting due to previous error
For more information about this error, try `rustc --explain E0499`.

悬垂引用dangling-references

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    // dangle 返回一个字符串的引用
    let s = String::from("hello"); // s 是一个新字符串

    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!

让我们尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免:

error[E0106]: missing lifetime specifier
 --> raii.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ help: consider giving it a 'static lifetime: `&'static`
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

error: aborting due to previous error
For more information about this error, try `rustc --explain E0106`.

错误信息引用了一个功能:生命周期(lifetimes)。

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String,Rust可不会允许我们这么做。

这里的解决方法是直接返回 String

fn no_dangle() -> String {
    let s = String::from("hello");
    s
}

slice类型

另一个没有所有权的数据类型是slice。slice允许你引用集合中一段连续的元素序列,而不用引用整个集合。


参考:

kaisery.github.io/trpl-zh-cn/… geniousbar.github.io/2019/08/25/…