08.Rust 语言实战笔记 —— 引用和借用

516 阅读8分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 2 天,点击查看活动详情

回顾下 07.Rust 语言实战笔记 —— 所有权 最后的例子

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 并移出给调用的函数
}

为了避免内存的不安全性,Rust 引入了所有权,但是也伴随着一个新的麻烦,就是需要把一个值传来传去的使用它。当变量传入一个函数,还要从函数中传出去时,就会导致语言表达变得十分啰嗦。

Rust 通过 借用(Borrowing) 这个概念来解决上述的问题,借用(borrowing)即获取变量的引用 。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。

引用与解引用

常规引用是一个指针类型,指向了对象存储的内存地址。在下面代码中,我们创建一个 i32值的引用 y,然后使用解引用运算符来解出 y 所使用的值:

fn main() {
    let x = 5;
    let y = &x;
    
    assert_eq!(5, x); 
    assert_eq!(5, *y);
}

变量 x 存放一个 i325yx 的一个引用。可以断言 x 等于 5。然而,若要对 y 做出断言,必须使用 *y 来进行解引用。一旦解引用 y,就可以访问 y 所指的数值,并进行比较。 这里可以参考 C 语言中的指针(*)取地址(&)的操作。

不可变引用

fn main() {
    let str = String:from("hello");
    let len = calculate_length(&str);
    println!("The length of '{}' is {}.", s1, len);
}

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

从上述代码中,细心的话可以发现,这里和之前的函数调用有两点不同:

  • 我们将 str 的引用传递到函数 calculate_length 中,而不是将 s1 的所有权传递到该函数。这使得代码更加简洁
  • calculate_length 的参数从之前的 String 变成 &String

这里,& 符号即使引用,它可以允许使用值,但是不能获得所有权。

通过 &str 语法,我们创建了一个指向 str 的引用,但是并不拥有它。因为并不拥有该变量的所有权,所以当引用离开作用域后,其指向的值也不会被丢弃。

同理,函数 calculate_length 使用 & 来表明参数 s 的类型是一个引用。

但是当你想要修改借用变量的值时,很不幸,这是无法修改的,因为之前见说过,在Rust中默认变量都是不可变类型,所以此处的引用也是不可变的。

举个例子:

fn main() {
    let str = String::from("hello");
    change(&str);
}

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

运行后会报错,报错内容如下:

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 String`
                           ------- 帮助:考虑将该参数类型修改为可变的引用: `&mut String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
                     `some_string`是一个`&`类型的引用,因此它指向的数据无法进行修改

可变引用

如果你了解了之前如何将不可变变量转换成可变变量,那么聪明的你一定会想到只需要小小的调整下,就可以对引用变量进行修改,从而解决上述问题。

fn main() {
    let mut str = String::from("hello");
    change(&mut s);
}

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

首先,声明 str 是可变类型,其次创建一个可变的引用 &mut str 和接受可变引用参数 some_string: &mut String 的函数。

当然对于这种可变借用,Rust 处于安全考虑会进行诸多的限制,也就是 borrow checker,下面我们来一起看看都有哪些限制

同一作用域,特定数据只能有一个可变引用

不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制: 同一作用域,特定数据只能有一个可变引用

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

    let s1 = &mut s;
    let s2 = &mut s;

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

此时代码会报错:

error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间无法对 `s` 进行两次可变借用
 --> src/main.rs:5:14
  |
4 |     let s1 = &mut s;
  |              ------ first mutable borrow occurs here 首个可变引用在这里借用
5 |     let s2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here 第二个可变引用在这里借用
6 |
7 |     println!("{}, {}", s1, s2);
  |                        -- first borrow later used here 第一个借用在这里使用

这段代码出错的原因在于,第一个可变借用 s1 必须要持续到最后一次使用的位置 println!,在 s1 创建和最后一次使用之间,我们又尝试创建第二个可变引用 s2

Rust 出于对安全的考虑,从而增加了这些限制,这种限制的好处就是可以防止 Rust 在编译期产生数据竞争。

可变引用与不可变引用不能同时存在

下面的代码会导致一个错误:

fn main() {
    let mut s = String::from("hello"); 
    let s1 = &s; // 没问题 
    let s2 = &mut s; // 大问题 
    println!("{}, {}", s1, s2);
}

此时代码会报错:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
        // 无法借用可变 `s` 因为它已经被借用了不可变
 --> src/main.rs:6:14
  |
4 |     let s1 = &s; // 没问题
  |              -- immutable borrow occurs here 不可变借用发生在这里
6 |     let s2 = &mut s; // 大问题
  |              ^^^^^^ mutable borrow occurs here 可变借用发生在这里
7 |
8 |     println!("{}, {}", s1, s2);
  |                                -- immutable borrow later used here 不可变借用在这里使用

其实这个也很好理解,正在借用不可变引用的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。多个不可变借用被允许是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染。

注意,引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }

举个例子:

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

    let s1 = &s;
    let s2 = &s;
    println!("{} and {}", r1, r2); 
    // s1,s2作用域在这里结束

    let s3 = &mut s;
    println!("{}", s3); 
    // s3作用域在这里结束
} 

在新版本的编译器中,因为将 引用作用域的结束位置从花括号变成最后一次使用的位置,所以 r1 借用和 r2 借用在 println! 后,就结束了,此时 r3 可以顺利借用到可变引用。

NLL

对于这种编译器优化行为,Rust 专门起了一个名字 —— Non-Lexical Lifetimes(NLL) ,专门用于找到某个引用在作用域(})结束前就不再被使用的代码位置。虽然减慢了开发速度,但是从长期来看,大幅减少了后续开发和运维成本。

悬垂引用(Dangling References)

悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 C 语言中,因悬垂指针导致的问题一般很难发现,所以出于安全的考虑,Rust 在编译期就禁止悬垂指针的出现。

举个例子:

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
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from 该函数返回了一个借用的值,但是已经找不到它所借用值的来源
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放, 但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!

其中一个很好的解决方法是直接返回 String

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

    s
}

这样就会将 s 的 所有权被转移给外面的调用者