Rust语言基础知识详解【五】

打印 上一主题 下一主题

主题 885|帖子 885|积分 2655

继上一篇对rust所有权的讲解之后,本节重要对接下来的引用与借用的知识做详细的先容。
上节中提到,假如仅仅支持通过转移所有权的方式获取一个值,那会让步伐变得复杂。 Rust 能否像其它编程语言一样,利用某个变量的指针或者引用呢?答案是可以。
Rust 通过 借用(Borrowing) 这个概念来告竣上述的目的,获取变量的引用,称之为借用(borrowing)。正如实际生活中,假如一个人拥有某样东西,你可以从他那里借来,当利用完毕后,也必须要物归原主。
引用与解引用

常规引用是一个指针类型,指向了对象存储的内存地址。在下面代码中,我们创建一个 i32 值的引用 y,然后利用解引用运算符来解出 y 所利用的值:
  1. fn main() {
  2.    let x = 5;
  3.    let y = &x;
  4.    assert_eq!(5, x);
  5.    assert_eq!(5, *y);
  6. }
复制代码
变量 x 存放了一个 i32 值 5。y 是 x 的一个引用。可以断言 x 即是 5。然而,假如盼望对 y 的值做出断言,必须利用 *y 来解出引用所指向的值(也就是解引用)。一旦解引用了 y,就可以访问 y 所指向的整型值并可以与 5 做比力。
相反假如尝试编写 assert_eq!(5, y);,则会得到如下编译错误:
  1. error[E0277]: can't compare `{integer}` with `&{integer}`
  2. --> src/main.rs:6:5
  3.   |
  4. 6 |     assert_eq!(5, y);
  5.   |     ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}` // 无法比较整数类型和引用类型
  6.   |
  7.   = help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for
  8.   `{integer}`
复制代码
不允许比力整数与引用,因为它们是差别的类型。必须利用解引用运算符解出引用所指向的值。
不可变引用

下面的代码,我们用 s1 的引用作为参数传递给 calculate_length 函数,而不是把 s1 的所有权转移给该函数:
  1. fn main() {
  2.    let s1 = String::from("hello");
  3.    let len = calculate_length(&s1);
  4.    println!("The length of '{}' is {}.", s1, len);
  5. }
  6. fn calculate_length(s: &String) -> usize {
  7.    s.len()
  8. }
复制代码
能注意到两点:

  • 无需像上章一样:先通过函数参数传入所有权,然后再通过函数返回来传出所有权,代码更加简洁
  • calculate_length 的参数 s 类型从 String 变为 &String
这里,& 符号即是引用,它们允许你利用值,但是不获取所有权,如图所示:

通过 &s1 语法,我们创建了一个指向 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用脱离作用域后,其指向的值也不会被丢弃。
同理,函数 calculate_length 利用 & 来表明参数 s 的类型是一个引用:
  1. fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
  2.    s.len()
  3. } // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
  4.  // 所以什么也不会发生
复制代码
假如尝试修改借用的变量呢?
  1. fn main() {
  2.    let s = String::from("hello");
  3.    change(&s);
  4. }
  5. fn change(some_string: &String) {
  6.    some_string.push_str(", world");
  7. }
复制代码
很不幸,你修改错了:
  1. error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
  2. --> src/main.rs:8:5
  3.   |
  4. 7 | fn change(some_string: &String) {
  5.   |                        ------- help: consider changing this to be a mutable reference: `&mut String`
  6.                           ------- 帮助:考虑将该参数类型修改为可变的引用: `&mut String`
  7. 8 |     some_string.push_str(", world");
  8.   |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  9.                     `some_string`是一个`&`类型的引用,因此它指向的数据无法进行修改
复制代码
正如变量默认不可变一样,引用指向的值默认也是不可变的,没事,来一起看看如何办理这个标题。
可变引用
只需要一个小调解,即可修复上面代码的错误:
  1. fn main() {
  2.    let mut s = String::from("hello");
  3.    change(&mut s);
  4. }
  5. fn change(some_string: &mut String) {
  6.    some_string.push_str(", world");
  7. }
复制代码
起首,声明 s 是可变类型,其次创建一个可变的引用 &mut s 和接受可变引用参数 some_string: &mut String 的函数。
可变引用同时只能存在一个

不外可变引用并不是随心所欲、想用就用的,它有一个很大的限定: 同一作用域,特定命据只能有一个可变引用
  1. let mut s = String::from("hello");
  2. let r1 = &mut s;
  3. let r2 = &mut s;
  4. println!("{}, {}", r1, r2);
复制代码
以上代码会报错:
  1. error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间无法对 `s` 进行两次可变借用
  2. --> src/main.rs:5:14
  3.   |
  4. 4 |     let r1 = &mut s;
  5.   |              ------ first mutable borrow occurs here 首个可变引用在这里借用
  6. 5 |     let r2 = &mut s;
  7.   |              ^^^^^^ second mutable borrow occurs here 第二个可变引用在这里借用
  8. 6 |
  9. 7 |     println!("{}, {}", r1, r2);
  10.   |                        -- first borrow later used here 第一个借用在这里使用
复制代码
这段代码堕落的缘故起因在于,第一个可变借用 r1 必须要连续到最后一次利用的位置 println!,在 r1 创建和最后一次利用之间,我们又尝试创建第二个可变借用 r2。
对于新手来说,这个特性绝对是一大拦路虎,也是新人们谈之色变的编译器 borrow checker 特性之一,不外各行各业都一样,限定通常是出于安全的思量,Rust 也一样。
这种限定的利益就是使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成:


  • 两个或更多的指针同时访问同一数据
  • 至少有一个指针被用来写入数据
  • 没有同步数据访问的机制
数据竞争会导致未界说行为,这种行为很可能超出我们的预期,难以在运行时追踪,并且难以诊断和修复。而 Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
许多时候,大括号可以帮我们办理一些编译不通过的标题,通过手动限定变量的作用域:
  1. let mut s = String::from("hello");
  2. {
  3.    let r1 = &mut s;
  4. } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
  5. let r2 = &mut s;
复制代码
可变引用与不可变引用不能同时存在

下面的代码会导致一个错误:
  1. let mut s = String::from("hello");
  2. let r1 = &s; // 没问题
  3. let r2 = &s; // 没问题
  4. let r3 = &mut s; // 大问题
  5. println!("{}, {}, and {}", r1, r2, r3);
复制代码
错误如下:
  1. error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  2.        // 无法借用可变 `s` 因为它已经被借用了不可变
  3. --> src/main.rs:6:14
  4.   |
  5. 4 |     let r1 = &s; // 没问题
  6.   |              -- immutable borrow occurs here 不可变借用发生在这里
  7. 5 |     let r2 = &s; // 没问题
  8. 6 |     let r3 = &mut s; // 大问题
  9.   |              ^^^^^^ mutable borrow occurs here 可变借用发生在这里
  10. 7 |
  11. 8 |     println!("{}, {}, and {}", r1, r2, r3);
  12.   |                                -- immutable borrow later used here 不可变借用在这里使用
复制代码
着实这个也很好理解,正在借用不可变引用的用户,肯定不盼望他借用的东西,被另外一个人莫名其妙改变了。多个不可变借用被允许是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染。
   注意,引用的作用域 s 从创建开始,一直连续到它最后一次利用的地方,这个跟变量的作用域有所差别,变量的作用域从创建连续到某一个花括号 }
  Rust 的编译器一直在优化,早期的时候,引用的作用域跟变量作用域是一致的,这对日常利用带来了很大的困扰,你必须非常小心的去安排可变、不可变变量的借用,免得无法通过编译,比方以下代码:
  1. fn main() {
  2.   let mut s = String::from("hello");
  3.    let r1 = &s;
  4.    let r2 = &s;
  5.    println!("{} and {}", r1, r2);
  6.    // 新编译器中,r1,r2作用域在这里结束
  7.    let r3 = &mut s;
  8.    println!("{}", r3);
  9. } // 老编译器中,r1、r2、r3作用域在这里结束
  10.  // 新编译器中,r3作用域在这里结束
复制代码
在老版本的编译器中(Rust 1.31 前),将会报错,因为 r1 和 r2 的作用域在花括号 } 处结束,那么 r3 的借用就会触发 无法同时借用可变和不可变 的规则。
但是在新的编译器中,该代码将顺利通过,因为 引用作用域的结束位置从花括号变成最后一次利用的位置,因此 r1 借用和 r2 借用在 println! 后,就结束了,此时 r3 可以顺利借用到可变引用。
NLL

对于这种编译器优化行为,Rust 专门起了一个名字 —— Non-Lexical Lifetimes(NLL),专门用于找到某个引用在作用域(})结束前就不再被利用的代码位置。
固然这种借用错误有的时候会让我们很郁闷,但是你只要想想这是 Rust 提前帮你发现了潜在的 BUG,着实就开心了,固然减慢了开发速度,但是从恒久来看,大幅淘汰了后续开发和运维成本。
悬垂引用(Dangling References)

悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍旧存在,其指向的内存可能不存在任何值或已被其它变量重新利用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的利用。
让我们尝试创建一个悬垂引用,Rust 会抛出一个编译时错误:
  1. fn main() {
  2.    let reference_to_nothing = dangle();
  3. }
  4. fn dangle() -> &String {
  5.    let s = String::from("hello");
  6.    &s
  7. }
复制代码
这里是错误:
  1. error[E0106]: missing lifetime specifier
  2. --> src/main.rs:5:16
  3.   |
  4. 5 | fn dangle() -> &String {
  5.   |                ^ expected named lifetime parameter
  6.   |
  7.   = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
  8. help: consider using the `'static` lifetime
  9.   |
  10. 5 | fn dangle() -> &'static String {
  11.   |                ~~~~~~~~
复制代码
错误信息引用了一个我们还未先容的功能:生命周期(lifetimes)。不外,纵然你不理解生命周期,也可以通过错误信息知道这段代码错误的关键信息:
  1. this function's return type contains a borrowed value, but there is no value for it to be borrowed from.
  2. 该函数返回了一个借用的值,但是已经找不到它所借用值的来源
复制代码
仔细看看 dangle 代码的每一步到底发生了什么:
  1. fn dangle() -> &String { // dangle 返回一个字符串的引用
  2.    let s = String::from("hello"); // s 是一个新字符串
  3.    &s // 返回字符串 s 的引用
  4. } // 这里 s 离开作用域并被丢弃。其内存被释放。
  5.  // 危险!
复制代码
因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放,但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!
其中一个很好的办理方法是直接返回 String:
  1. fn no_dangle() -> String {
  2.    let s = String::from("hello");
  3.    s
  4. }
复制代码
这样就没有任何错误了,终极 String 的 所有权被转移给表面的调用者

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

天空闲话

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表