Bootstrap

【第六课】Rust所有权系统(二)

目录

前言

借用和引用

借用规则

切片和迭代器

总结


前言

上节课介绍了Rust中的所有权系统,简单回顾一下,rust的内存系统系统,每一块内存都有一个主人,主人对这块内存有着读写和释放的权限,当主人离开作用域之后,该内存被自动释放。内存的主人会转移,当内存中的值是复制语义时,主人不会转移,会复制一份在内存中给新的主人,当内存中的值是移动语义时,主人会转移。在大部分编程中移动语义的值会多一些,比如vector,hashmap,都是我们高频使用的集合,还有String,也是高频使用,我们下面举一个例子来看看使用移动语义的数据时,会有什么不方便的地方,也引出今天的主题:引用和借用。

我们的例子是计算一个字符串的长度,非常常见的功能。

使用一个函数计算字符串的长度,这段代码build后会报错的(报错截图贴在下面),为什么会报错呢?我们一行一行的看一看,首先hello-rust在变量绑定给my_str后,my_str是这一块内存的主人,然后我们使用函数计算了字符串的长度,这里注意了,编译器在这里也给出了详细的解释,我们将my_str传递给函数的时候,input其实转移了所有权,在这里hello-rust的所有权从my_str转移给了input,当函数运行结束,input走出了作用域的时候,由于input是hello-rust的主人,所以触发内存清理,那么问题来了,当我们在println中使用my_str时,因为my_str已经不是任何内存的主人了,所以编译不让通过。那如果我们想要完成这样的功能怎么办呢?这里的核心问题出现在移动语义的值一定会造成所有权转移,即使我们并不希望转移所有权,比如目前的例子中,上一节课中我们使用了字典的例子,我们字典的主人是A,每次字典的交接主人都会变,这是不太好的,为什么别人不能来借用字典呢?主人依然是A,但是别的通过可以借用,当前的例子也是一样,如果函数参数input只是借用my_str,而不是替代my_str,那么就可以在后面继续使用my_str。

fn main() {
    let my_str = String::from("hello-rust");
    let len = get_length(my_str);

    println!("{} len = {}", my_str, len);
}

fn get_length(input: String) -> usize {
    input.len()
}

借用和引用

上面的例子中,我们提到了一个重要的概念,就是借用,就好像同学B借用同学A的字典一样,同学B借同学A的字典,这个动作叫做借用,借到了之后,我们称同学B有字典的引用,这就是借用和引用的概念,默认的借用和引用都是不可变的,意思就是同学B只能翻阅字字典,但是不能在字典上做笔记。如果同学B可以在字典上做笔记,那么这个行为叫做可变借用,拿到的也是可变引用。

我们使用不可变借用来重写一下上面的代码,使其不报错。

先简单介绍一下代码,&my_str是获取不可变引用的语法,函数的形参使用&修饰,此时input获取的是my_str的不可变引用,有读权限。

fn main() {
    let my_str = String::from("hello-rust");
    let len = get_length(&my_str);

    println!("{} len = {}", my_str, len);
}

fn get_length(input: &String) -> usize {
    input.len()
}

再看看可变引用,我们完成一个需求,在字符串后面添加字符串,如果要对原始的字符串执行写操作,就必须使用可变借用获取到可变引用,可变借用使用&mut表示,代码如下。

fn main() {
    let mut my_str = String::from("hello-rust");

    add_str(&mut my_str);

    println!("my_str = {}", my_str);

}

fn add_str(input: &mut String) {
    input.push_str("-good")
}

借用规则

首先,明确一下借用和所有权,借用无法获取所有权,借用不会变更主人。

借用其实很像读写锁

不可变引用是读锁,可变引用是写锁,在某一个时刻,要么是n个读锁,要么是1个写锁。

切片和迭代器

切片,slice,是引用的一种使用代表,切片其实就是一段引用,并不会拥有所有权。我们可以对字符串、vector、数组,使用切片获取一段数据的引用。

下面是切片的基本使用语法,使用&变量名[start..end]的方式获取一段数据的引用,start和end分别对应开始和结束位置,左闭右开,包头不包尾。

fn main() {
    let my_str = String::from("hello-rust");
    let hello = &my_str[0..5];

    let my_arr = [1, 2, 3, 4, 5];
    let sub_arr = &my_arr[0..];

    let my_vec = vec![1, 2, 3, 4, 5];
    let sub_vec = &my_vec[0..3];

}

可变切片,切片是引用的实际应用,由于引用有可变,所以切片也存在可变切片。

如下代码,首先需要将my_vec声明为mut,因为切片是一种引用,修改切片其实在修改底层的数据,使用&mut获取一个可变的引用,修改数据后,打印my_vec的结果,会发现第一个元素被修改为90了。

fn main() {
    let mut my_vec = vec![1, 2, 3, 4, 5];
    let sub_vec = &mut my_vec[0..3];
    sub_vec[0] = 90;
    println!("{:?}", my_vec);
}

引用的第二个常见应用就是迭代器。

(1)转移所有权

下面的代码使用into_iter()会将集合中元素的所有权转移到迭代器中,所以println!想再次访问my_vec会报错。

fn main() {
    let mut my_vec = vec![1, 2, 3, 4, 5];

    for i in my_vec.into_iter() {
        println!("i = {}", i);
    }

    println!("{:?}", my_vec);
}

(2)不可变引用

使用iter()获取集合的不可变迭代器,遍历之后原集合依然可以访问

fn main() {
    let mut my_vec = vec![1, 2, 3, 4, 5];

    for i in my_vec.iter() {
        println!("i = {}", i);
    }

    println!("{:?}", my_vec);

}

(3)可变引用

使用iter_mut()方法可以获取集合的可变引用,通过*i可以访问到对应的数据,在rust中&表示创建引用,*表示解引用,访问到对应的值,我们通过*i将原来的值都做了加1操作。

fn main() {
    let mut my_vec = vec![1, 2, 3, 4, 5];

    for i in my_vec.iter_mut() {
        println!("i = {}", i);
        *i = *i + 1;
    }

    println!("{:?}", my_vec);
}

总结

这节课我们讲了rust中的借用和引用,借用是行为,引用是结果,避免了只能所有者才能操作数据导致的所有权转移问题,再次强调下三者的关系:

所有者:对数据有读写和释放内存的权限

不可变引用:对数据有读权限

可变引用:对数据有读写权限

在此基础上,介绍了rust中常见的关于引用的用例,切片和迭代器。

;