走进Rust:参考和借阅

Rust大约 6761 字

参考和借阅

上一节元组代码的问题在于,我们必须将String返回给调用函数,因此,在调用calculate_length之后,我们仍然可以使用String,因为将String移入了calculate_length

以下是如何定义和使用calculate_length函数,这个函数传入的是对象的引用作为参数而不是所有权:

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()
}

首先,请注意,变量声明和函数返回值中的所有元组代码都消失了。 其次,请注意,我们将&s1传递给calculate_length,并且在其定义中,我们采用&String而不是String

这些与符号是引用,它们使你可以引用某些值而无需拥有所有权。 如下图所示,&String s指向String s1

trpl0405.png

注意:使用&进行引用的反义词是解引用,通过运算符*完成解引用。我们将在第8章中看到解引用运算符的一些用法,并在第15章中讨论解引用的细节。

让我们仔细看看这里的函数调用:

let s1 = String::from("hello");

let len = calculate_length(&s1);

通过&s1语法,我们可以创建一个引用s1的引用,但该引用不拥有s1。因为&s1不拥有s1,所以当引用超出作用域时,&s1所指向的值将不会被删除。

同样,函数的签名使用&表示参数s的类型是引用。 让我们添加一些说明性注释:

fn calculate_length(s: &String) -> usize { // s 是 String 类型的引用
    s.len()
} // 这一行,s 超出作用域。但因为 s 没有拥有它引用的对象的所有权,所以啥事情都不会发生(即不发生 drop )

变量s有效的作用域与任何函数参数的作用域相同,但是当它超出作用域时,由于没有所有权,我们不会删除引用指向的内容。当函数使用引用作为参数而不是实际值作为参数时,我们不需要返回这些值来归还所有权,因为我们从来没有所有权。

我们称引用为函数参数借用。与现实生活中一样,如果某人拥有某物,则可以向他们借用。完成后,你必须将其归还。

那么,如果我们尝试修改要借用的内容会怎样?

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

    change(&s);
}

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

会得到以下错误:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut std::string::String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

正如变量在默认情况下是不可变的一样,引用也是如此。我们不允许修改引用的内容。

可变引用

我们可以通过少量调整来修复上述代码中的错误:

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接收一个可变引用。

但是可变引用有一个很大的限制:在一个特定作用域内,一个特定的数据只能有一个可变引用。下列代码将编译失败:

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

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

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

得到以下错误:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.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`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

该限制允许改变,但是以非常受控的方式。新手很难解决这一问题,因为大多数语言都可以让你随时随地进行改变。

具有此限制的好处是Rust可以防止在编译时发生数据竞争。数据竞争类似于竞争条件,并且在以下三种行为发生时发生:

  • 两个或多个指针同时访问相同的数据。
  • 至少有一个指针用于写入数据。
  • 没有被用于访问数据时上锁的机制。

数据竞争会导致未定义的行为,并且在尝试在运行时进行跟踪时可能会难以诊断和修复;Rust不会发生此问题,因为它对于有数据竞争的代码甚至都不会通过编译!

与平常一样,我们可以使用大括号创建新的作用域,从而允许多个可变引用,而不能同时引用:

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

    {
        let r1 = &mut s;
    } // r1 在这一行超出作用域,所以我们可以创建一个新的引用

    let r2 = &mut s;
}

对于组合可变引用和不可变引用,存在类似的规则。 以下代码会报错:

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

    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    let r3 = &mut s; // 大问题

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

得到以下错误:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
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 | 
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

当我们声明了不变引用时,我们也不能可变引用。声明了不变引用的用户不会期望这个值从它们的代码之后突然改变!但是,可以使用多个不可变引用,因为仅仅是读取数据去无法影响其他任何人对数据的读取。

请注意,引用的作用域从引入它的地方开始,一直持续到最后一次使用该引用。

例如,下列代码将被编译通过,因为不可变引用的最后一次使用发生在引入可变引用之前:

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

    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    println!("{} and {}", r1, r2);
    // r1 和 r2 在这之后不再使用

    let r3 = &mut s; // 没问题
    println!("{}", r3);
}

不可变引用r1r2的作用域在println!之后结束,并且是在创建可变引用r3之前。这些作用域不重叠,因此允许使用此代码。

即使借用错误有时可能令人沮丧,但请记住,Rust编译器尽早(在编译时而不是在运行时)指出了潜在的错误,并确切地指出了问题出在哪里。然后,你不必追踪为什么数据不是你想像的那样。

悬空引用(空指针)

在带有指针的语言中,很容易错误地创建一个悬空指针,这个指针指向的内存中的位置可能已经分配给他人了(通过释放内存并保留内存的指针来分配的)。相比之下,在Rust中,编译器保证引用永远不会出现悬空引用:如果你有对某些数据的引用,则编译器将确保该数据不会在引用该数据之前超出作用域。

让我们尝试创建一个悬空的引用,Rust将通过编译时错误防止该引用:

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

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

    &s
}

会得到以下错误:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.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`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

此错误消息指的是我们尚未涵盖的功能:生命周期。我们将在第10章中详细讨论生命周期。但是,如果你忽略有关生命周期的部分,则错误信息中确实包含导致此代码出现问题的关键:

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 函数返回一个 String 类型的引用

    let s = String::from("hello"); // s 是一个新的 String 类型变量

    &s // 我们返回一个 String 类型的引用 s
} // 这一步,s 超出作用域并且并销毁。它的内存释放了。
  // 报错!

因为s是在dangle内部创建的,所以当dangle的代码完成时,将释放s。但是我们试图返回它的引用。这意味着此引用将指向无效的StringRust不允许我们这样做。

解决方案是直接返回String

fn main() {
    let string = no_dangle();
}

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

    s
}

这可以正常工作。 所有权被移出,没有任何东西被释放。

引用的规则

让我们回顾一下我们对引用的讨论:

  • 在任何给定时间,你都可以声明一个可变引用或任意数量的不可变引用。
  • 引用必须始终有效。

接下来,我们将看看另一引用:切片。

阅读 55 · 发布于 2020-07-07

————        END        ————

扫描下方二维码关注公众号和小程序↓↓↓

昵称:
随便看看换一批