错误处理
错误分类:
- 可恢复错误,例如文件未找到,可再次尝试
- 不可恢复错误,bug,例如访问的索引超出范围
Rust 没有类似异常的机制,针对可恢复错误提供了 Result<T, E>
,不可恢复错误提供了 panic!
宏。
panic!
宏执行的时候,程序会打印一个错误信息,展开(unwind)、清理调用栈(Stack),退出程序。
默认情况下,当 panic
发生:
- 程序展开调用栈(工作量大),Rust 沿着调用找往回走,清理每个遇到的函数中的数据。
- 或立即中止调用找,不进行数据清理,内存需要 OS 进行清理
想让二进制文件更小,把设置从“展开”改为“中止”:在 Cargo.toml
中适当的 profile 部分设置 panic='abort'
。
Cargo.toml 中加入:
[profile.release]
panic = 'abort'
复制代码
要获取回溯信息,运行的时候使用:
# win
set RUST_BACKTRACE=1 && cargo run
# linux 和 mac
RUST_BACKTRACE=1 cargo run
复制代码
示例代码:
fn main() {
let v = vec![0, 1, 2, 3];
println!("{}", v[33]);
}
// 或
fn main() {
panic!("error");
}
复制代码
Result 枚举
处理可能失败的情况。
enum Result<T, E> {
Ok(T), // 操作成功
Err(E), // 操作失败
}
复制代码
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Error opening file {:?}", error)
}
};
}
复制代码
unwrap 可以简化 match
操作,如果 Result 是 Ok,返回 Ok 里面的值,如果 Result 是 Err,调用 panic!
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
复制代码
expect 和 unwrap
类似,但是可以执行 panic!
宏打印的信息:
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("panic info");
}
复制代码
use std::{fs::File, io::ErrorKind};
// match 虽然可以用但是比较复杂,可以改为闭包,后面再学
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Error creating file: {:?}", e),
},
other_error => panic!("Error opening file {:?}", other_error),
}
};
println!("{:?}", f);
}
复制代码
传播错误
将错误发回给调用者,而不是在函数中直接处理。
use std::{fs::File, io::{self, Read}};
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
fn main() {
let result = read_username_from_file();
println!("{:?}", result);
}
复制代码
?
运算符可以简化上面的操作:
如果 Result 是 Ok,Ok 中的值就是表达式的结果,然后继续执行程序;
如果 Result Err,Err 就是整个函数的返回值,就像使用了 return。
use std::{fs::File, io::{self, 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)
}
fn main() {
let result = read_username_from_file();
println!("{:?}", result);
}
复制代码
// 链式调用进一步简化
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
复制代码
?
运算符只用用于返回 Result 的函数。
Trait std::convert::From 函数,用于错误之间的转换,?
运算符就会隐式的被 from
处理。当 ?
调用 from
函数时,它所接收的错误类型会被转化为当前函数返回类型所定义的错误类型。
适用于不同错误原因,返回同一种错误类型的场景(只要每个错误类型实现了转换为所返回的错误类型的 from
函数就行)。
哪些场景比较时候 panic
- 演示某些概念:upwrap
- 原型代码:unwrap、expert
- 测试代码:unwrap、expert
- 有时候你比编译器掌握更多信息,你可以确定 Result 就是 Ok:unwrap
错误处理的指导性建议
- 调用你的代码,传入无意义的参数值:panic!
- 调用外部不可控代码,返回非法状态,你无法修复:panic!
- 如果失败是可预期的:Result
- 当你的代码对值进行操作,首先应该验证这些值:panic!
泛型
提高代码的复用能力。泛型的类型参数(或者叫占位符)会在编译器编译的时候将占位符替换为具体的类型。
函数定义中的泛型
fn dosomething<T>(param: T) -> T {
param
}
fn dosomething2<T>(param: &T) -> &T {
param
}
fn main() {
dosomething(12);
dosomething2(&String::from("abc"));
}
复制代码
Struct 中使用泛型
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.2, y: 23.1 };
}
复制代码
Enum 定义中的泛型
可以让枚举的变体持有泛型数据类型。
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
复制代码
方法中使用泛型
impl<T> Point<T> { // 把 T 放在 impl 关键字后,表示在类型 T 上实现方法
fn x(&self) -> &T {
&self.x
}
}
复制代码
泛型代码的性能和使用具体类型的代码运行速度是一样的,因为在编译的时候会执行单态化(monomorphization),将泛型类型替换为具体类型。
Trait
Trait 告诉 Rust 编译器某种类型具有哪些并且可以与其它类型共享的功能。Trait 抽象的定义共享行为
Trait bounds(约束):泛型类型参数指定为实现了特定行为的类型。
Trait 与其它语言的接口(interface)类似,但有些区别。
定义一个 Trait
把方法签名放在一起,来定义实现某种目的所必需的一组行为。使用 trait
关键字,只有方法签名,没有具体实现。
Trait 可以有多个方法,每个方法签名占一行,以分号结尾。实现该 trait 的 类型必须提供具体的方法实现。
pub trait Summary {
fn summarize(&self) -> String;
fn summarize1(&self) -> String;
}
复制代码
在类型上实现 Trait
与为类型实现方法类似,但是也有一些不同之处。
// main.rs
// test_rust 是 Cargo.toml 中 package 下的 name
use test_rust::Summary; // summarize 在 Summary 这个 trait 上定义的
use test_rust::Tweet;
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
复制代码
// lib.rs
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
复制代码
实现 trait 的约束
可以在某个类型上实现某个 trait 的前提条件是:这个类型 或 这个 trait 是在本地 crate 里定义的。
无法为外部类型来实现外部的 trait:
- 这个限制是程序属性的一部分(也就是一致性)
- 更具体地说是孤儿规则:之所以这样命名是因为父类型不存在
- 此规则确保其他人的代码不能破坏你的代码,反之亦然
- 如果没有这个规则,两个 crate 可以为同一类型实现同一 trait,Rust 就不知道应该使用哪个实现了
默认实现
pub trait Summary {
// fn summarize(&self) -> String;
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
// 注释部分可以不写,如果写了就代表重写了默认实现
impl Summary for NewsArticle {
// fn summarize(&self) -> String {
// format!("{}, by {} ({})", self.headline, self.author, self.location)
// }
}
复制代码
默认实现的方法可以调用 trait 中其它的方法,即使这些方法没有默认实现。
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
String::from("(Read more from {} ...)", self.summarize_author())
}
}
impl Summary for NewsArticle {
fn summarize_author(&self) -> String {
format!("@{}", self.author)
}
}
复制代码
注意:无法从方法的重写实现里面调用默认的实现。
Trait 作为参数
impl Trait
语法:适用于简单情况
pub fn notify(item: impl Summary) { // 实现了 Summary trait 的某个类型
println!("Breaking news! {}", item.summarize());
}
复制代码
Trait bound
语法:可用于复杂情况
pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}
复制代码
使用 +
指定多个 Trait
pub fn notify<T: Summary + Display>(item: T) {
println!("Breaking news! {}", item.summarize());
}
复制代码
Trait bound
使用 where
子句,可以在方法签名后指定 where 子句
// 这个签名很长,不直观
pub fn notify<T: Summary + Display, U: Clone + Debug>(a: T, b: U) -> String {
println!("Breaking news! {}", a.summarize());
}
// 更好的写法
pub fn notify<T, U>(a: T, b: U) -> String
where
T: Summary + Display,
U: Clone + Debug
{
println!("Breaking news! {}", a.summarize());
a.summarize()
}
复制代码
Trait 作为返回类型
使用 impl Trait
方式
pub fn notify(s: &str) -> impl Summary {
NewsArticle {
headline: String::from("Penguins"),
content: String::from("The ..."),
author: String::from("Iceburgh"),
location: String::from("USA"),
}
}
复制代码
注意:在使用 impl Trait
方式作为返回类型的时候只能返回一种类型,不能返回多种可能的类型。
使用 Trait Bound
方式
fn largest<T: PartialOrd + Clone>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list.iter() {
if item > &largest {
largest = item;
}
}
largest
}
复制代码
使用 Trait Bound 有条件的实现方法
在使用泛型参数的 impl
块儿上使用 Trait Bound
,我们可以有条件的为实现了特定 Trait 的类型来实现方法。
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> { // T 必须实现 Display + PartialOrd
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest number is x = {}", self.x);
} else {
println!("The largest number is y = {}", self.y);
}
}
}
复制代码
也可以为实现了其它 Trait 的任意类型有条件的实现某个 Trait。
为了满足 Trait Bound
的所有类型上实现 Trait 叫做覆盖实现(blanket implementations)。
// 标准库中 string.rs 的代码
impl<T: fmt::Display + ?Sized> ToString for T { // 实现了 Display 方法的类型都要实现 ToString 这个 Trait
#[inline]
default fn to_string(&self) -> String {
let mut buf = String::new();
let mut formatter = core::fmt::Formatter::new(&mut buf);
// Bypass format_args!() to avoid write_str with zero-length strs
fmt::Display::fmt(self, &mut formatter)
.expect("a Display implementation returned an error unexpectedly");
buf
}
}
复制代码
生命周期
Rust 的每个引用都有自己的生命周期,在生命周期内,引用保持有效的作用域。大多数情况下,生命周期是隐式的、可推断的。当引用的生命周期可能以不同的方式互相关联时,则需要手动标注生命周期。
生命周期的主要目标就是避免悬垂引用(dangling reference)。
fn main() {
let r;
{
let x = 5;
r = &x; // 报错:`x` does not live long enough
}
println!("r: {}", r);
}
复制代码
上面的错误是通过 借用检查器
判断出来的,它比较作用域来判断所有的借用是否合法。
函数的泛型生命周期
fn main() {
let str1 = String::from("abcd");
let str2 = "xyz";
let result = longest(str1.as_str(), str2);
println!("The longest string is {}", result);
}
// missing lifetime specifier
fn longest(x: &str, y: &str) -> &str { // 报错:expected named lifetime parameter
if x.len() > y.len() {
x
} else {
y
}
}
复制代码
报错原因是缺少生命周期的标注,函数的返回值包含一个借用的值,但是函数的签名没有说明是从 x
借用的,还是从 y
借用的。考虑引入一个命名的生命周期参数解决这个问题。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // 用 'a 对生命周期进行了标注就不报错了
if x.len() > y.len() {
x
} else {
y
}
}
复制代码
生命周期的标注语法
- 生命周期的标注不会改变引用的生命周期长度
- 当指定了泛型生命周期参数,函数可以接收带有任何生命周期的引用
- 生命周期的标注描述了多个引用的生命周期的关系,但不影响生命周期
语法:生命周期参数名都是以 '
开头,通常全小写开头且非常短,很多人使用 'a
。
生命周期标注的位置:在引用的 &
符号后,使用空格将标注和引用类型分开。
&i32
一个引用&'a i32
带有显式生命周期的引用&'a mut i32
带有显式生命周期的可变引用
单个生命周期标注本身没有意义。
泛型生命周期参数声明在函数名和参数列表之间的 <>
里。
生命周期 'a
的实际生命周期是 x
和 y
两个生命周期中较小的那个。
Struct 定义中的生命周期标注
Struct 里可包括自持有的类型,需要在每个引用上添加生命周期标注。
struct ImportantExcerpt<'a> {
part: &'a str, // 是引用类型的字段,所以需要标注生命周期,否则会报错
}
fn main() {
let novel = String::from("Call me Ishmael. Som years ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not found a '.'");
let i = ImportantExcerpt {
part: first_sentence
};
}
复制代码
生命周期的省略
我们知道每个引用都有生命周期,需要为使用生命周期的函数或 struct 指定生命周期参数。但是下面这个例子没有标注任何生命周期,还是可以通过编译。
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
复制代码
上面这段代码生命周期最早是需要标注的 fn first_word<'a>(s: &'a str) -> &'a str
,但是随着 Rust 版本演进和发展,对某些固定的生命周期做了优化,所以这里就不需要显式的标注生命周期了。
生命周期省略规则:在 Rust 引用分析中所编入的模式称为生命周期省略规则。这些规则无需开发者来遵守,它们是一些特殊情况,由编译器来考虑,如果你的代码符合这些情况,那么就无需显式标注生命周期。
生命周期省略规则不会提供完整的推断,如果应用规则后,引用的生命周期仍然模糊不清就会导致编译错误,这时就需要添加生命周期标注,表明引用间的相互关系。
输入、输出生命周期:生命周期在函数/方法的参数上为输入生命周期,生命周期在函数/方法的返回值上为输出生命周期。
生命周期省略的三个规则:编译器使用 3 个规则在没有显示标注生命周期的情况下,来确定引用生命周期。
规则 1 应用于输入生命周期;
规则 2、3 应用于输出生命周期;
如果编译器应用完 3 个规则之后,仍然无法确定生命周期的引用就会报错。
这些规则适用于 fn 定义和 impl 块。
规则如下:
- 规则1:每个引用类型的参数都有自己的生命周期。
- 规则2:如果只有 1 个 输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数。
- 规则3: 如果有多个输入生命周期参数,但其中一个是
&self
或&mut self
(是方法),那么 self 的生命周期会被赋给所有的输出生命周期参数。
方法定义中的生命周期标注
在 struct 上使用生命周期实现方法,语法和泛型参数的语法一样。在哪儿声明和使用生命周期参数依赖于生命周期参数是否和字段、方法的参数或返回值有关。
struct 字段的生命周期名在 impl 后声明,在 struct 名后使用,这些生命周期式 struct 类型的一部分。
impl 块内的方法签名中,引用必须绑定于 struct 字段引用的生命周期,或者引用是独立的也可以,生命周期省略规则经常使得方法中的生命周期标注不是必须的。
struct ImportantExcerpt<'a> {
part: &'a str, // 是引用类型的字段,所以需要标注生命周期,否则会报错
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Som years ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not found a '.'");
let i = ImportantExcerpt {
part: first_sentence
};
}
复制代码
静态生命周期
'static
是一个特殊的生命周期:整个程序的持续时间。
例如:所有的字符串字面值都拥有 'static
生命周期
let s: &'static str = "abcd"; // 无需每次都显式声明
复制代码
泛型参数类型、Trait Bound、生命周期 都使用的一个例子:
fn longest_with_an_announcement<'a, T>
(x: &'a str, y: &'a str, ann: T) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {}
复制代码