admin 管理员组

文章数量: 887007

Rust 语言基础—引用与借用

Rust 语言基础


七、引用与借用

1、引用的基本概念

  • 学过其他编程语言的,如 C++ 语言的,应该对引用这个概念有所了解。
  • 在 C++ 中函数的传参分为传值传参和传址传参,前者无论函数体内做什么操作不会对实参的值造成影响;而后者往往会影响实参的值。Rust 中的引用也是传参的形式,如下实例:
    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()
    }
    
  • 可以看到,函数 calculate_length(s: &String) -> usize 的参数使用了 “s: &String” 的声明形式,这边是 Rust 语言中引用式传参的声明形式,符号 ”&“ 告诉编译器在这个函数中以引用的方式传入实参。
  • 一旦函数声明引用式传参,则意味着接下来在该函数体中可以使用实参的值但不获得实参的所有权,从而避免发生移动。
  • 以这个例子来讲,如果不声明为引用式传参,那么将会发生移动,在语句 ”let len = calculate_length(&s1)“ 中,s1 将会移动给 s,从而在语句 ”let len = calculate_length(&s1)“ 接下来的语句中,s1 将失去作用,而 s 在calculate_length(s: &String) -> usize 的函数体之外也是无效的,这样一来,便将失去 s1 的再次使用的机会,也就是说接下来的语句 ”println!(“The length of ‘{}’ is {}.”, s1, len);“ 将会报错,无法编译通过。
    error[E0382]: borrow of moved value: `s1`--> src\main.rs:4:44|
    2 |     let s1 = String::from("hello");|         -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
    3 |     let len = calculate_length(s1);|                                -- value moved here
    4 |     println!("The length  of '{}' is {}.", s1, len);|                                            ^^ value borrowed here after moveerror: aborting due to previous error
    
  • &s1 语法让我们创建一个 指向 值 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
  • 同理,函数签名使用 & 来表明参数 s 的类型是一个引用。
    fn main() {
    fn calculate_length(s: &String) -> usize { // s 是对 String 的引用s.len()
    } // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,// 所以什么也不会发生
    }
    
  • 变量 s 有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据,因为没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。
  • 将获取引用作为函数参数称为 借用(borrowing)。

2、不可变引用

  • 与 C++ 不同的是,引用不允许修改实参的值,只能使用实参的值,可以说 Rust 语言中的 ”&“ 与 C++ 语言的作用相反;当我尝试对借用的变量做出修改值得操作如下例子,那么编译器将会报错。
    fn main() {let s1 = String::from("hello");change(&s1);
    }
    fn change(s:&String) {s.push_str(", world");
    }
    
  • 编译器报错如下:
    error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference--> src\main.rs:8:5|
    7 | fn change(s:&String) {|             ------- help: consider changing this to be a mutable reference: `&mut std::string::String`
    8 |     s.push_str(", world");|     ^ `s` is a `&` reference, so the data it refers to cannot be borrowed as mutableerror: aborting due to previous error
    
  • 正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。

3、可变引用

  • 如果是必须对传入的参数的值进行操作的话,只需在不可变引用的基础上,进行修改,增加一个关键字 mut 将参数声明为:s:&mut String(参数类型),那么就能在函数体里对参数的值进行修改了,如下例子:
    fn main() {let mut s1 = String::from("hello");change1(&mut s1);println!("s1:{}",s1);//change(&s1);/*let len = calculate_length(&s1);println!("The length  of '{}' is {}.", s1, len);*/
    }fn change1(s: &mut String) {s.push_str(", I'm trying!!!");
    }
    // 输出如下:
    // s1:hello, I'm trying!!!
    
  • 但是可变引用有一个很大的麻烦,不是想用就能用,可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。
  • 如下例子,将无法编译通过:
    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--> src/main.rs:5:10|
    4 | let r1 = &mut s;|          ------ first mutable borrow occurs here
    5 | let r2 = &mut s;|          ^^^^^^ second mutable borrow occurs here
    6 | println!("{}, {}", r1, r2);|                    -- borrow later used here
    
  • 这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条件,它可由这三个行为造成:
    1)两个或更多指针同时访问同一数据。
    2)至少有一个指针被用来写入数据。
    3)没有同步数据访问的机制。
  • 数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
  • 不过,可以通过大括号来创建一个新的作用域,从而可以拥有多个可变引用:
    fn main() {let mut s = String::from("hello");{let r1 = &mut s;} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用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
    println!("{}, {}, and {}", r1, r2, r3);
    
  • 错误如下:
    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
    
  • 简单来说,允许有多个不可变引用,但只允许有一个可变饮用,在特定作用域中。

4、悬垂引用(Dangling References)

  • 在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
  • 在 Rust 语言中,如果我们尝试创建一个悬垂引用,那么 Rust 会通过一个编译时错误来避免
    fn main() {let reference_to_nothing = dangle();
    }
    fn dangle() -> &String {let s = String::from("hello");&s
    }
    
  • 错误如下:
    error[E0106]: missing lifetime specifier--> src\main.rs:19:16|
    19 | 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 fromerror: aborting due to previous error
  • 仔细说明的就是如下:
    fn dangle() -> &String { // dangle 返回一个字符串的引用let s = String::from("hello"); // s 是一个新字符串&s // 返回字符串 s 的引用
    } // 这里 s 离开作用域并被丢弃。其内存被释放。// 危险!
  • 解决方法就是直接返回 String,也就是把函数体里的最后一句改成 ”s“ 就行了。

5、引用的规则

  • 概括起来,引用的规则有如下:
    1)在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
    2)引用必须总是有效。

本文标签: Rust 语言基础引用与借用