Rust快速入门(五)
生命周期
生命周期的主要作用是避免悬垂引用。
这里我们详细说说借用检查:
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
rust使用了一个借用检查器来检查程序正确性,它会比较两个变量声明周期,比如上述代码中:r的生命周期是 'a
,x的生命周期是'b
,由于a引用了b,由于b远远小于a,所以编译器会拒绝执行上述代码。
函数中的生命周期
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
上述代码在rust中会报错,因为在longest函数中,编译器无法知道该函数的返回值到底引用 x
还是 y
。
也可以手动定义两个生命周期的长度规范:
impl<'a: 'b, 'b> ImportantExcerpt<'a> {
fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
println!("Attention please: {}", announcement);
self.part
}
}
上述<>内容表示 :
-
'a
的生命周期至少和'b
一样长 -
'a
必须比'b
活得更久或同样长
也可以这样做:
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
where
'a: 'b,
{
println!("Attention please: {}", announcement);
self.part
}
}
所以这里引出一个概念:在存在多个引用时,编译器有时会无法自动推导生命周期,此时就需要我们手动去标注,通过为参数标注合适的生命周期来帮助编译器进行借用检查的分析。
生命周期标注语法
生命周期标注并不会改变任何引用的实际作用域。标记生命周期只是为了取悦编译器,不让编译器为难我们。
生命周期的语法以'
开头:
&i32 // 普通引用
&'a i32 // 具有显示生命周期的引用
&'a mut i32 // 具有显示生命周期的可变引用
声明的规则:
-
和泛型一样,使用生命周期参数,需要先声明
<'a>
-
x
、y
和返回值至少活得和'a
一样久(因为返回值要么是x
,要么是y
)
fn useless<'a>(x: &'a i32, y: &'a i32) -> &'a str {}
举一个例子:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
上述代码中,返回值的生命周期与参数生命周期中的较小值一致,虽然两个参数的生命周期都是标注了 'a
,但是实际上这两个参数的真实生命周期可能是不一样的(生命周期 'a
不代表生命周期等于 'a
,而是大于等于 'a
)。
函数的返回值如果是一个引用类型,那么它的生命周期只会来源于:
-
函数参数的生命周期
-
函数体中某个新建引用的生命周期
结构体中的生命周期
与函数类似,不过多赘述
生命周期消除
对于编译器来说,每一个引用类型都有一个生命周期,编译器为了简化用户的使用,运用了生命周期消除大法。
注意:
-
消除规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期
-
函数或者方法中,参数的生命周期被称为
输入生命周期
,返回值的生命周期被称为输出生命周期
三条消除规则:
-
每一个引用参数都会获得独自的生命周期
-
若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期
-
若存在多个输入生命周期,且其中一个是
&self
或&mut self
,则&self
的生命周期被赋给所有的输出生命周期
静态生命周期
'static
,拥有该生命周期的引用可以和整个程序活得一样久。
返回值与错误处理
panic
panic!()
panic时两种终止方式:栈展开和直接终止
默认的方式就是 栈展开
,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。直接终止
,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。
线程panic如果不是main线程,那么程序就不会终止。
可恢复的错误Result
使用枚举Result:
enum Result<T, E> {
Ok(T),
Err(E),
}
实际例子:
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error)
},
};
}
失败直接崩溃的 panic: unwrap 和 expect
如果返回成功,就将 Ok(T)
中的值取出来,如果失败,就直接 panic
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
except相当于重载错误打印函数。
传播错误宏:?
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
它的作用跟 match
几乎一模一样