Rust学习(四):作用域、所有权和生命周期:
Rust引入了所有权、作用域和生命周期等概念,并通过这些概念限制了各种量的转台和作用范围,Rust的作用域在所有编程语言中是最严格的,变量只能按照所有权和生命周期在某个代码块中生效,一旦超出这个范围,变量就会自动释放,Rust的作用域和C语言中的作用域一样,都是以“{}”作为边界,真正难以学习的是Rust的所有权机制和生命周期组成的作用域规则,下面我们重点介绍一下Rust中的所有权机制和生命周期。
1、所有权机制:
①定义:
所有权机制是Rust中的内存管理手段,类似于python中的垃圾回收机制或者是C/C++中的手动释放内存,Rust的编译器会根据一系列规则进行检查,倘若违反了规则,程序连编译器都无法通过(也造就了C/C++编译之后无法允许,Rust无法编译的梗)。
所有权机制规则如下:
- 每个值只有一个所有者
- 值在任意时刻都只有一个所有者
- 当所有者离开作用域时,值会被自动丢弃。
实际上,Rust是将垃圾回收机制赋予了变量本身,这样不仅更安全,也更节约内存,维持了负载均衡,可以参考下面这个例子:
fn main() {
let long = 10;
{
// 设置一个临时作用域:
let a = 5;
println!("a的值为:{}", a);
} // a从临时作用域出来,根据所有权规则 a 被丢弃, println!("a:{}", a); 将会报错
println!("long的值为:{}", long);
}
③移动和借用:
这里需要区分的是:移动是将变量的所有权转移给了新的变量,而借用所有权依然为原变量所拥有,新变量只是原来变量的一个引用:
fn main() {
let x = "vertex".to_string();
let y = x; // 将堆上的变量x的所有权转移给了变量 x 此时 再访问 x 将会报错
let a = "geek".to_string();
let b = &a;
println!("{a}, {b}"); //将a通过引用借给了b,b不用于a的所有权,因此访问a和b都不会报错
let c = &mut a; //设置可变引用
// let d = &mut a; 错误!一个值只能有一个可变引用
}
④拷贝和克隆:
通过拷贝和克隆,相当于是将一本电子书复印了一份,这样原变量和新变量都拥有了对值得所有权(相当于原版和印刷版):
fn main() {
let a = "string".to_string();
let b = a.clone(); //克隆
println!("{a} and {b}");
let x = 10; //在栈上创建得变量
let y = x;
println!("{x} and {y}"); //在栈上创建得数据,rust默认实现了一个 Copy trait 相当于赋予了变量拷贝得功能,因此不需要再使用clone方法
}
2、生命周期:
熟悉C/C++的小伙伴,一定对指针不陌生,rust中的引用功能十分强大,用途上非常类似于C/C++中的指针,因此为了避免出现和C/C++中一样的悬垂指针(这里应该是悬垂引用),rust引入了生命周期的概念:
fn main() {
let a;
{
let b = 10;
a = &b;
}
println!("{a}"); //错误!出现了悬垂引用。
}
这里遇到了一个问题:b在它自身的作用域结束之后就被释放了,也就是说a被赋值给了一个指向为”空“的引用,这就发生了悬垂引用,编译器不会给我们的代码放行!
fn longest<'a>(x: &'a String, y: &'a String) ->&'a String {
if x.len() < y.len() {
y
} else {
x
}
}
在编写了很多 Rust 代码后,Rust 团队发现在特定情况下 Rust 程序员们总是重复地编写一模一样的生命周期注解。这些场景是可预测的并且遵循几个明确的模式。接着 Rust 团队就把这些模式编码进了 Rust 编译器中,如此借用检查器在这些情况下就能推断出生命周期而不再强制程序员显式的增加注解。
这里我们提到一些 Rust 的历史是因为更多的明确的模式被合并和添加到编译器中是完全可能的。未来只会需要更少的生命周期注解。
被编码进 Rust 引用分析的模式被称为 生命周期省略规则(lifetime elision rules)。这并不是需要程序员遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期。
省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。编译器会在可以通过增加生命周期注解来解决错误问题的地方给出一个错误提示,而不是进行推断或猜测。
函数或方法的参数的生命周期被称为 输入生命周期(input lifetimes),而返回值的生命周期被称为 输出生命周期(output lifetimes)。
编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 fn
定义,以及 impl
块。
第一条规则是编译器为每一个引用参数都分配一个生命周期参数。换句话说就是,函数有一个引用参数的就有一个生命周期参数:fn foo<'a>(x: &'a i32)
,有两个引用参数的函数就有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
,依此类推。
第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32
。
第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 &self
或 &mut self
,说明是个对象的方法 (method)那么所有输出生命周期参数被赋予 self
的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。
所有权和生命周期的概念确实复杂且难以理解,需要初学者花费大量的时间和精力!