目录
前言
上节课介绍了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中常见的关于引用的用例,切片和迭代器。