引用与借用

ch04-02-references-and-borrowing.md
commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

在上一部分的结尾处的使用元组的代码是有问题的,我们需要将String返回给调用者函数这样就可以在调用calculate_length后仍然可以使用String了,因为String先被移动到了calculate_length

下面是如何定义并使用一个(新的)calculate_length函数,它以一个对象的引用作为参数而不是获取值的所有权:

Filename: src/main.rs

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

    let len = calculate_length(&s1);

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

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

首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递&s1calculate_length,同时在函数定义中,我们获取&String而不是String

这些 & 符号就是引用,他们允许你使用值但不获取它的所有权。图 4-8 展示了一个图解。

&String s pointing at String s1

Figure 4-8: &String s pointing at String s1

仔细看看这个函数调用:

# fn calculate_length(s: &String) -> usize {
#     s.len()
# }
let s1 = String::from("hello");

let len = calculate_length(&s1);

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

同理,函数签名使用了&来表明参数s的类型是一个引用。让我们增加一些解释性的注解:

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, nothing happens.

变量s有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有它。

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

那么如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!

Filename: src/main.rs
fn main() {
    let s = String::from("hello");

    change(&s);
}

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

Listing 4-9: Attempting to modify a borrowed value

这里是错误:

error: cannot borrow immutable borrowed content `*some_string` as mutable
 --> error.rs:8:5
  |
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^

正如变量默认是不可变的,引用也一样。不允许修改引用的值。

可变引用

可以通过一个小调整来修复在列表 4-9 代码中的错误,在列表 4-9 的代码中:

Filename: src/main.rs

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

    change(&mut s);
}

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

首先,必须将s改为mut。然后必须创建一个可变引用&mut s和接受一个可变引用some_string: &mut String

不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。这些代码会失败:

Filename: src/main.rs

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

let r1 = &mut s;
let r2 = &mut s;

具体错误如下:

error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> borrow_twice.rs:5:19
  |
4 |     let r1 = &mut s;
  |                   - first mutable borrow occurs here
5 |     let r2 = &mut s;
  |                   ^ second mutable borrow occurs here
6 | }
  | - first borrow ends here

这个限制允许可变性,不过是以一种受限制的方式。新 Rustacean 们经常与此作斗争,因为大部分语言任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争(data races)。

数据竞争是一种特定类型的竞争状态,它可由这三个行为造成:

  1. 两个或更多指针同时访问相同的数据。
  2. 至少有一个指针被用来写数据。
  3. 没有被用来同步数据访问的机制。

数据竞争会导致未定义行为并且当在运行时尝试追踪时可能会变得难以诊断和修复;Rust 阻止了这种情况的发生,因为存在数据竞争的代码根本就不能编译!

一如既往,使用大括号来创建一个新的作用域,允许拥有多个可变引用,只是不能同时拥有:

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

{
    let r1 = &mut s;

} // r1 goes out of scope here, so we can make a new reference with no problems.

let r2 = &mut s;

当结合可变和不可变引用时有一个类似的规则存在。这些代码会导致一个错误:

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

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM

错误如下:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
 --> borrow_thrice.rs:6:19
  |
4 |     let r1 = &s; // no problem
  |               - immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |                   ^ mutable borrow occurs here
7 | }
  | - immutable borrow ends here

哇哦!我们不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个读取数据的人有能力影响其他人读取到的数据。

即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。

悬垂引用

在存在指针的语言中,容易错误地生成一个悬垂指针dangling pointer),一个引用某个内存位置的指针,这个内存可能已经因为被分配给别人,因为释放内存时指向内存的指针被保留了下来。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

让我们尝试创建一个悬垂引用:

Filename: src/main.rs

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

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

这里是错误:

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

error: aborting due to previous error

错误信息引用了一个我们还未涉及到的功能:生命周期lifetimes)。第十章会详细介绍生命周期。不过,如果你不理会生命周期的部分,错误信息确实包含了为什么代码是有问题的关键:

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from.

让我们仔细看看我们的dangle代码的每一步到底放生了什么:

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!

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

正确的代码是直接返回String

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

    s
}

这样就可以没有任何错误的运行了。所有权被移动出去,所以没有值被释放掉。

引用的规则

简要的概括一下对引用的讨论:

  1. 特定时间,只能拥有如下中的一个:
  • 一个可变引用。
  • 任意属性的不可变引用。
  1. 引用必须总是有效的。

接下来,我们来看看一种不同类型的引用:slices。