文章会通过三个例子来了解 rust 所有权的机制,都是从应用的角度来说明,rust 所有权底层是如何实现会在后续的内容中介绍。
理解RUST
所有权规则:
-
RUST
中的每个值都有一个对应的变量作为它的所有者 - 同一时间内,值有且仅有一个所有者
- 当所有者离开自己的作用域时,它持有的值就会被释放掉
基础篇
下面的示例代码编译不通过,在 s1 赋值给变量 s2 的过程中,字符串 neojos 值的所有权由 s1 转移给了 s2,然后 s1 变成了未初始化的状态。
fn main() {
let s1: String = "neojos".to_string();
let s2 = s1;
println!("print s1:{}", s1)
}
假设是 go 语言来实现同样的代码,程序没有任何问题,s1 和 s2 实际指向的同样的字符串,可以正常打印 s1,赋值前后压根不会对 s1 产生影响。而 rust 偏偏要引入所有权的概念,内存中的一个值只能属于一个变量,最终,字符串值属于了 s2 之后,s1 也就相当于被垃圾回收了。
快速解决上述问题的方案,是将赋值的方式修改为引用赋值,就像下面这样,给 s2 赋值的是 s1 的引用,这样 s2 就不能接管 neojos 字符串的所有权,引用赋值过程就属于 借用。下面的方式属于只读借用,最终读取的还是 s1 。
还可以是 &mut 的可写引用,如果要想修改指针的值,就必须声明为可写引用。
fn main() {
let s1: String = "neojos".to_string();
let s2 = &s1;
println!("print s1:{}", s1)
}
如果我们取引用的值,会发生值的移动吗?比如下面的例子,s2 指向了 s1 的可修改引用,s3 试图通过从 s2 取值,那么,s1 变量所拥有的值会发生转移吗?
fn main() {
let mut s1: String = "neojos".to_string();
let s2 = &mut s1;
let s3: String = *s2;
println!("print s1:{}", s1);
}
代码编译会发生报错,报错的信息见截图。看 move occurs 的提示,感觉 *s2 发生了所有权转移,但又补充没有实现 Copy
特性。既然 *s2 属于值类型,就应该发生所有权转移,但提示没有实现 Copy 特性,赋值语句怎么变成了 Copy 特性了呢?只有基本类型在赋值的过程中,才会发生值拷贝,String 类型的赋值不应该是值拷贝。
从类型上推断,s1 的类型是 mut String,s2 的类型是 &mut String,s3 的类型是 String。 然后从多个角度思考这个问题:
① rust 中 &mut 是排它操作,在 s2 的生命期内不应该有其它值能重新引用到 s1,s3 试图在转移 s1;
② 有没有什么途径可以使这个例子正常编译?
丈二和尚摸不着头脑,我们继续简化一下上面的例子。按照常规的处理思路先来验证一番,通过编译器的提示来了解 rust 的处理思路。编译器给我们提供了“暗示”的能力,帮我们直观推测出变量的类型。
fn main() {
let mut s1: String = "neojos".to_string();
let s2 = &mut s1;
s1 = "change name".to_string();
print!("{}", s2)
}
编译器的类型暗示能力:
在给 s1 重新赋值的过程中,编译器报错:cannot assign to s1 because it is borrowed
,s2 借用了变量 s1,在 s2 变量没有被回收之前,rust 限制不能对 s1 进行操作。在控制台执行 rustc --explain E0506
查看官方的解释
在 s2 引用没有释放之前,s1 不能被赋予一个新值。这样的限制保证了 s2 引用的值不会发生改变,但 rust 底层是怎么实现这样的限制的?这样的处理操作有点类似 MySQL 事务版本的特性,在 s2 的生命期内,它读到的值不会发生变化。
fn main() {
let mut s1: String = "neojos".to_string();
let s2 = &mut s1;
drop(s2);
s1 = "change name".to_string();
print!("{}", s2)
}
这次强制 drop 掉变量 s2,这里其实是盲目地使用 drop 函数。正常来说,当一个变量离开它的作用域时,该类型实现的 drop 函数会自动被调用,属于析构函数的性质。
这是 drop 函数的解释说明:This method is called implilcitly when the value goes out of scope, and cannot be called explicitly (this is compiler error E0040). 奇怪,我这种主动调用的形式,也没有什么编译问题。
之前还只是一个报错,在之前的机器上,现在多出来一个报错
- error[E0506]: cannot assign to
s1
because it is borrowed 不能给一个被借用的值重新赋值 - error[E0382]: borrow of moved value:
s2
,借用了一个所有权发生转移的值。
这么看来,drop
函数并没有起到它的作用,我们还可以添加作用域来触发 drop 函数,但对应的代码就需要做调整。下面的代码可以正常编译运行,但已经不是原来的代码了。之前代码的意图是:
- 给 s1 赋值
- 将 s2 设置为 s1的引用
- 修改 s1 的值
- 打印 s2 的值
再看看下面的代码,虽然编译成功了,但已经和我想验证的南辕北辙了。通过上述的过程可以发现,rust 禁止了这样的过程,我们在引用一个变量的时候,完全不需要担心变量的值会发生更改。
fn main() {
let mut s1: String = "neojos".to_string();
{
let s2 = &mut s1;
print!("{}", s2)
}
s1 = "change name".to_string();
}
升级篇
下面的例子可以正常编译运行,输出结果为 false。第一次接触 rust 的话,对 &4、&false 这样的写法会感到奇怪,Go 语言是不可以对常量取地址的。
例子在做的事情很简单,但我们还是详细介绍一下代码细节。在业务代码中,将数组转换为字典算是一个比较常规的操作,数组查询需要遍历整个数组,而字典只需要O(1)的时间复杂度。
- 使用 use 将 HashMap 引入到当前作用域,HashMap 默认不在标准库中,这一点还是挺意外的
- 使用 vec 宏创建一个数组,调用 new 方法来创建一个空的字典
- 遍历数组,通过 insert 来向字典中添加元素
- 判断常量 4 是否在字典中,如果在的话,is_ok 返回 true,否则返回 false
- 打印 is_ok 的结果
use std::collections::HashMap;
fn main() {
let exist_types = vec![1, 2, 3];
let mut dicts = HashMap::new();
for index in exist_types {
dicts.insert(index, true);
}
let is_ok = match dicts.get(&4) {
None => &false,
Some(res) => res,
};
print!("is exist:{}", is_ok);
}
在 for 循环之后,我们尝试输出数组 exist_types 数组,很遗憾,程序编译报错,变量 exist_types 在 for 循环遍历中发生了所有权转移。就是说,for 循环执行完成之后,数组就变成初始化状态了。
如下代码,①标注了我们增加打印输出的位置。报错提示: borrow of moved value: exist_types
。格式化宏 print! 中的参数并不会发生变量所有权转移,它只会借用参数的共享引用。
use std::collections::HashMap;
fn main() {
let exist_types = vec![1, 2, 3];
let mut dicts = HashMap::new();
for index in exist_types {
dicts.insert(index, true);
}
// ①
print!("{:?}\n", exist_types);
let is_ok = match dicts.get(&4) {
None => &false,
Some(res) => res,
};
print!("is exist:{}", is_ok);
}
既然这样,下面将 for 遍历的对象修改为数组的引用。for 语句的遍历对象变成了数组的引用,程序可以正常编译执行,数组也得到了正常的打印。
可别小看这一个地方的修改,因为这个地方的修改,字段的类型也会发生变化。之前字段的类型是 HashMap<i32,bool>,现在的类型是 HashMap<&i32,bool>。还需要特别注意另外一点,遍历 exist_types 的元素类型是 i32,遍历 &exist_types 的元素类型是 &i32。
use std::collections::HashMap;
fn main() {
let exist_types = vec![1, 2, 3];
let mut dicts = HashMap::new();
// ②
for index in &exist_types {
dicts.insert(index, true);
}
print!("{:?}\n", exist_types);
let is_ok = match dicts.get(&4) {
None => &false,
Some(res) => res,
};
print!("is exist:{}", is_ok);
}
如果想继续保持 dicts 的类型是 HashMap<i32,bool>,我们可以在 insert 的时候对引用取值,修改图中 ③标注的位置。程序依旧可以正常执行。但其实多了一丝困惑,为什么在 insert 的时候对 *index 取地址,并没有发生所有权的转移。
本来确实应该发生所有权转移的,但因为这个类型是基础的 int32 类型,基础的数字类型在相互赋值的时候不会发生所有权转移,赋值过程会其实是拷贝。
既然这样,有必要将数组的类型调整成字符串类型,再来验证一番。
use std::collections::HashMap;
fn main() {
let exist_types = vec![1, 2, 3];
let mut dicts = HashMap::new();
for index in &exist_types {
// ③
dicts.insert(*index, true);
}
print!("{:?}\n", exist_types);
let is_ok = match dicts.get(&4) {
None => &false,
Some(res) => res,
};
print!("is exist:{}", is_ok);
}
在上面例子的基础上,我们转换为字符串来验证。为了方便理解,特意将变量的类型做了明确声明,其实吧,编译可以默认帮我们做了这些事情。
程序还是可以正确运行,说明数组中元素的所有权并没有转移到 HashMap 中。问题来了,为什么没有进行所有权的转移呢?问题出在数组的元素类型上,因为数组的元素类型是 &str,本身就是一个引用类型,在整个程序的执行过程中,都不会发生所有权的转移。
既然知道的问题的原因,我们继续通过改造这个例子来验证参测。没有必要继续使用 HashMap 这个结构,我们可以通过更简单的例子来验证数组中变量的所有权转移。
use std::collections::HashMap;
fn main() {
let exist_types: Vec<&str> = vec!["1", "2", "3"];
let mut dicts: HashMap<&str, bool> = HashMap::new();
for index in &exist_types {
dicts.insert(*index, true);
}
print!("{:?}\n", exist_types);
let is_ok: &bool = match dicts.get("4") {
None => &false,
Some(res) => res,
};
print!("is exist:{}", is_ok);
}
看下面这个例子,首先,明确声明了变量 first ,类型为 String。继续对数组引用进行遍历,只不过,这次我们强制使用 **index 来获取引用的值。我想:既然 index 类型是 &&str,那直接使用 **index 势必可以获取到 str 类型,对 **index 进行赋值操作,应该可以触发所有权转移了吧。
事与愿违,编译器有报错提示,又触发了另外一个问题,str 变量的尺寸未知。
- the size for values of type
str
cannot be known at compilation time
这么看来,对 str 这个类型还真是没有办法了,直接声明为 str 类型,编译器无法确定它的内存大小,声明为 &str 又是一个引用类型,无法触发所有权转移。既然如此,现在唯一的办法就是使用 String 这个类型
fn main() {
let first = "1".to_string();
let exist_types: Vec<&str> = vec![&first, "2", "3"];
let take_first: str;
for index in &exist_types {
take_first = **index;
break;
}
print!("{:?}\n", exist_types);
}
下面的代码和前面的 HashMap 代码唯一的差别是:数组类型变成 String 了,对应的 HashMap 的类型也得跟着调整。迫不及待的想要试验一把了。文章来源:https://www.toymoban.com/news/detail-590740.html
🙂!有编译报错提示,index 是一个共享的引用,不能对它做所有权转移。这个和 Go 的设计非常接近,在 for 循环的迭代过程中,变量 index 其实是一个值。文章来源地址https://www.toymoban.com/news/detail-590740.html
- cannot move out of
*index
which is behind a shared reference
use std::collections::HashMap;
fn main() {
let exist_types: Vec<String> = vec!["1".to_string(), "2".to_string(), "3".to_string()];
let mut dicts: HashMap<String, bool> = HashMap::new();
for index in &exist_types {
dicts.insert(*index, true);
}
print!("{:?}\n", exist_types);
let is_ok: &bool = match dicts.get("4") {
None => &false,
Some(res) => res,
};
print!("is exist:{}", is_ok);
}
到了这里,关于rust 引用怎么用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!