rust语言基础学习: rust所有权之引用和借用

rust语言基础学习: rust所有权之引用和借用

2020-07-09
Rust

昨天学习了rust中的Move语义和Copy语义。先做一个简单的复习:

  • Move语义: 当进行变量赋值、函数传参、函数返回时,如果涉及变量的类型没有实现Copy trait,就会使用Move语义转移值的所有权,失去所有权的变量将失效,在其作用域内从所有权转移位置以后将无法再被使用。
  • Copy语义:当进行变量赋值、函数传参、函数返回时,如果涉及变量的类型实现了Copy trait,就会将值自动复制(自动按位浅拷贝)一份,产生新的值,原有的变量在其作用域内还能被继续使用。

不希望所有权转移的方案: 手动复制堆上的数据(不推荐) #

Move语义下的单一所有权虽然解决了其他编程语言中堆内存上数据被随意引用的问题,但也会带来使用上的不便,因为失去所有权的变量将会失效无法被继续使用,例如下面的示例代码:

1fn main() {
2    let word = String::from("hello");
3    let len = calculate_length(word);
4    println!("{}'s len is {}", word, len) // compile error: borrow of moved value: `word`
5}
6
7fn calculate_length(s: String) -> usize {
8    s.len()
9}

当String数据的所有权从变量word转移到calculate_length函数中的s时,main函数中的word将无法继续被使用。 如果想在调用完calculate_length函数后,继续使用word变量,该怎么办呢? String类型没有实现Copy trait无法使用Copy语义,但是String类型实现了std::clone::Clone trait:

 1#[cfg(not(no_global_oom_handling))]
 2#[stable(feature = "rust1", since = "1.0.0")]
 3impl Clone for String {
 4    fn clone(&self) -> Self {
 5        String { vec: self.vec.clone() }
 6    }
 7
 8    fn clone_from(&mut self, source: &Self) {
 9        self.vec.clone_from(&source.vec);
10    }
11}

可以调用word.clone()将数据手动复制一份传参给calculate_length函数的s,这样堆内存上就有两份String数据,分别别变量words所有,互不影响。代码如下:

1fn main() {
2    let word = String::from("hello");
3    let len = calculate_length(word.clone());
4    println!("{}'s len is {}", word, len) 
5}
6
7fn calculate_length(s: String) -> usize {
8    s.len()
9}

上面代码执行到calculate_length函数内部时,进程的虚拟内存布局示意图如下:

rust-ownership-showcase-5.png

但是使用调用clone函数避免变量所有权转移的方式有很大弊端,手动复制很麻烦,更大的问题是复制的效率不是很高。

通过前面的学习,我们已经知道了两种不希望所有权转移的方法:

  • 实现了Copy trait的类型,赋值和传参时会使用Copy语义,复制一份数据,不会发生所有权转移
  • 对于使用Move语义的类型,手动调用clone函数复制堆上的数据(这种方式效率很差,不推荐)

手动clone的方式显然没有解决问题,那么在Rust中如果既希望所有权不被转移,又无法使用Copy语义时,该怎么办呢? Rust提供了Borrow语义,即不希望所有权转移时,可以借用数据。

Borrow语义 #

借用就是一个值的所有权在不发生转移的情况下,借给其他变量使用。 在Rust中要使用借用,需要先使用引用语法(&&mut)创建一个引用。注意这里的引用,要同其他编程语言中的引用区分开。在其他编程语言(例如Java)中,一个值的多个引用拥有对值的访问权限,相当于共享了所有权。在Rust中,创建的引用只是拥有临时的使用权,而没有所有权。

借用的定义:Rust中将创建一个引用的行为称为借用(borrowing)

1fn main() {
2    let word = String::from("hello");
3    let len = calculate_length(&word);
4    println!("{}'s len is {}", word, len) 
5}
6
7fn calculate_length(s: &String) -> usize { // s是对String的引用
8    s.len()
9} // 这里s离开了作用域,但s并不拥有所引用值的所有权,s离开作用域时,引用的值并不会被丢弃

上面的代码,第3行&word创建了一个word的只读引用,这个创建引用的行为称为借用,同时注意calculate_length函数的参数类型是&String,而不是String。上面代码执行到calculate_length函数内部时,进程的虚拟内存布局示意图如下:

rust-ownership-showcase-6.png

上图中s的ptr是word变量的地址,wordptr是String堆内存数据的地址。

&word创建的是一个只读引用,如果想在不获取所有权的前提下修改word的数据,可以使用&mut word创建一个可变引用,下面给出示例代码,注意示例代码中update_string函数的参数类型是&mut String:

1fn main() {
2    let mut word = String::from("hello");
3    update_string(&mut word);
4}
5
6fn update_string(s: &mut String) {
7    s.push_str(" world")
8}

借用规则 #

借用规则指的是在Rust中使用借用语义时的一些限制,即创建引用时的限制:

  1. 引用必须总是有效的,引用不能超过值的生命周期,编译器会确保我们编写的代码不会创建悬垂引用
  2. 在任意给定的时间,一个值,要么只能有一个活跃的可变引用,要么只能有多个不可变引用

引用不能超过值的生命周期 #

悬垂引用是指:创建的引用还在,但其引用数据的内存已经释放且后边可能已经被分配给其他所有者。 Rust的编译器会确保引用永远不会变成悬垂状态。

下面的代码尝试创建一个悬垂引用,dangle函数内s离开作用域后String数据会被释放,但函数返回了它的引用,Rust是不允许我们这么做的,在编译时直接报错。注意报错信息是expected lifetime parameter,是后边我们要学习的生命周期的知识,所有权和生命周期是学习Rust的基础。

1fn main() {
2    let reference_to_nothing = dangle();
3}
4
5fn dangle() -> &String { // compile error: expected lifetime parameter
6    let s = String::from("hello");
7    &s
8}

在借用值时,不能在同一时刻创建多个可变引用 #

注意第2条限制中,“活跃"一词的含义是,创建的可变引用用来修改值了才是活跃的,如果创建的可变引用被当做不可变引用来使用,不算活跃,只能当做一般的不可变引用。例如下面的代码,是编译无法通过的:

1fn main() {
2    let mut x = 100;
3    let y = &mut x;
4    let z = &mut x; // compile error: cannot borrow `x` as mutable more than once at a time
5    *y += 100;
6    *z += 1000;
7    assert_eq!(x, 1200);
8}

这段代码中为x创建了2个可变引用y和z,紧跟着就使用x和z去修改x的值了(*x*y是解引用的语法),在使用y进行修改时,z还是活跃的,也就是在同一时间内x存在2个活跃的可变引用,这是不允许的。这段代码修改成下面这样就能编译通过了:

1fn main() {
2    let mut x = 100;
3    let y = &mut x;
4    *y += 100;
5    let z = &mut x;
6    *z += 1000;
7    assert_eq!(x, 1200);
8}

在创建可变引用z之前,完成了对可变引用y的使用,这样从可变引用z创建出来之后,y没有被再使用过,y不再活跃,因此可以编译通过。

下面的在借用时,同时创建了可变引用和不可变引用,编译无法通过:

1fn main() {
2    let mut x = 100;
3    let y = &x;
4    let z = &mut x; // compile error: cannot borrow `x` as mutable because it is also borrowed as immutable
5    println!("{:}", y);
6    *z += 1000;
7}

总结 #

本节我们学习了借用Borrow的语义,借用就是一个值的所有权在不发生变化转移的情况下,借给其他变量使用。 要借用就需要先创建引用(可变引用或不可变引用),Rust中将创建一个引用的行为称为借用(borrowing)。

在Rust中创建引用有两个限制,即借用规则:

  1. 引用必须总是有效的
  2. 在任意给定的时间,一个值,要么只能有一个活跃的可变引用,要么只能有多个不可变引用

这两个规则的描述有些拗口,尤其是第2条,我们可以从不满足这些限制条件时,编译器的错误信息来理解,个人觉得直接用编译的提示信息作为规则描述更好一些:

  1. 引用不能超过值的生命周期 (expected lifetime parameter,编译器提示缺少生命周期参数,生命周期是我们后续学习的内容)
  2. 在同一时刻一个值不能创建多个可变引用(cannot borrow x as mutable more than once at a time)
  3. 在同一时刻一个值已经创建了不可变引用,不能再为其创建可变引用(cannot borrow x as mutable because it is also borrowed as immutable)

参考 #

© 2024 青蛙小白