今天是学习rust的第四天。学习材料为官网的《The Rust Programming Language》本笔记包括第九章错误处理和第十章:generics,traits and lifetimes
panic
当程序出现rust无法处理的bug时,会启动panic
宏,程序打印一个失败信息,unwind并清空栈,随后退出。
unwind有时是一个耗费性的工作,如果想要程序立刻终止,不清空内存,可以在 C a r g o . t o m l Cargo.toml Cargo.toml文件中添加命令:
[profile.release]
panic = 'abort'
此时清空内存的工作交给操作系统。
result枚举类型:
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
T、E为数据类型,T为result返回成功时要输出的数据类型,E为失败时的类型。
fn main(){
let f = File::open("hello.txt");
let f = match f { //成功则打开文件,不成功则根据错误类型进行判断
Ok(file) => file,
Err(error) => match error.kind(){ //如果是不存在该文件,则创建新文件,其他错误则panic!
ErrorKind::NotFound => match File::create("hello.txt") { //新创建文件成功返回文件,不成功panic!
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error=> panic!("Problem opening the file: {:?}", other_error)
},
};
}
以上代码可以解决一部分问题,但是三次match的使用使得代码冗长,可读性降低。引入unwrap
解决这个问题。若成功,unwrap
会返回Ok中的值,若失败,unwrap
会调用panic!
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
另一个方法为expect
,在chapter2使用过,可以返回我们想要的错误信息:
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
相比之下,由于expect
方法会返回我们规定的错误信息,再调用panic!,因此错误比较容易找到,而unwrap
则较难确定错误的源头。(但是写的时候更简洁)
错误传递:
函数read_username_from _file函数不清楚该如何处理出现的错误,可以将错误传递给调用它的函数,让调用函数处理,成为错误传递
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), //不需要显示声明return,因为是函数的最后一行了
}
}
这个情况在rust中非常常见,因而引入了?
操作符,可以使得代码更简洁:
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的功能几乎相同。区别是?
操作符会调用标准库中的from
函数。
上述代码还可以继续简化为:
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或Option的函数,像main函数返回()
就不能直接使用?
,需要修改如下:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> { //修改main函数的返回值
let f = File::open("hello.txt")?;
Ok(())
}
此处的Box类型称为trait object,将在17章详述。此处可以理解为“任何形式的错误”。注意main函数的唯一合法返回值为()
。
panic
or Not to panic
不在预期范围内的error可以调用panic
,预料中的error则应返回Result
generic,个人理解有点像template in C,
如果要将重复代码替换为函数,基本过程如下:
与函数不同的是,generic支持抽象的数据类型
在函数定义中使用generic,通常将generics放在一般定义参数数据类型的位置和返回值的位置。通常情况下习惯使用T
:
fn largest<T>(list: &[T]) -> T { //函数largest在类型T上generic,该函数具有一个参数:list,是类型T的slice,该函数返回值类型为T
同样可以令结构体使用generics,结构体定义可以使得其中的不同元素有不同的类型,但是类型太多也会带来阅读代码的困难:
struct Point<T, U> { //定义generic结构体,x和y可以有不同的类型
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
在前几章中我们已经见到了一些枚举类型的generics,例如单变量类型的:
enum Option<T> {
Some<T>,
None,
}
多变量的:
enum Result<T, E> {
Ok(T),
Err(E),
}
struct Point<T> { //首先定义一个结构体
x: T,
y: T,
}
impl<T> Point<T> { //在implementation中,使用generic,这个函数x是一个get函数,将private的变量传递出去
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
结论:使用generics不会拖慢程序的速度。rust通过monomorphization来实现generics,该过程指在编译时“填空”,将generic中的抽象类型变为具体类型。
个人理解:有点像接口 interface
t
r
a
i
t
trait
trait 告诉编译器一个特定的类型具有的、能够分享给其他类型的功能。
一种类型的“behavior”包括我们可以调用它的所有方法。
不同的类型有相同的behavior如果我们可以在这些类型中调用同样的方法。Trait定义使得我们可以把方法(method signatures)组合起来,来定义一组behavior。
pub trait Summary { //关键字:trait,名字:Summary
fn summarize(&self) -> String; //方法,不给出细节,以分号结束
}
trait的实现(implementation)与普通方法的实现没有区别。
File name : src/lib.rs
pub trait Summary { //定义trait
fn summarize(&self) -> String; //定义trait中的方法
}
pub struct NewsArticle { //定义结构体NewsAticle
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle { //为结构体NewsArticle实现trait
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {//定义结构体Tweet
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {//为结构体Tweet实现trait
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
注意我们不可以在对外部类型定义外部的traits,例如在Vec<T>
上定义实现Display
,这个限制称为coherence
或“orphan rule”
在定义trait时,如果有具体方法的实现细节,则为“default implementation”。默认的实现方法不会影响后续的代码,新的方法会重写(override)旧的方法。
default implementation可以调用同个trait中其他的方法,即便这些方法没有具体default implementation。
traits同样可以作为别的函数的参数:
pub fn notify(item: impl Summary) { //参数为impl Summury
println!("Breaking news! {}", item.summarize()); //调用trait summary的方法:summarize
}
上述代码实际上是下面这段代码的语法糖:(解释:语法糖syntax suger指更优化的代码,实现相同的功能,耗费相同的资源,更简洁、流畅、具备可读性)
pub fn notify<T: Summary>(item: T) { //成为trait bound
println!("Breaking news! {}", item.summarize());
}
将所有的trait bound都写清楚会导致函数定义冗长,可读性下降,采用where从句的方法解决这个问题:
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone, //where 从句写清trait bound
U: Clone + Debug
{
等价于
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
举一个例子进行分析:假设我们希望实现函数largest,能够得到数组中最大的元素,我们可以这样实现:
fn largest_i32 (list: &[i32]) -> i32{ //实现一个针对i32类型的最大值函数
let mut largest = list[0];
for &item in list.iter(){
if item > largest {
largest = item;
}
}
largest
}
fn largest_char (list: &[char]) -> char { //实现一个针对char类型的最大值函数
let mut largest = list[0];
for &item in list.iter(){
if item > largest{
largest = item;
}
}
largest
}
fn main() { //主函数
let number_list = vec![34, 50, 25, 100, 65]; //分别调用两个函数
let result = largest_i32(&number_list);
println!("The largest number is {}.", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {}.", result);
}
读以上代码会发现,针对i32类型的最大值函数和针对char类型的最大值函数具有很大的重复性,为了增强代码的简洁性,引入generics:
fn largest<T> (list: &[T]) -> T{ //引入generics之后的largest函数,类型抽象为T
let mut largest = list[0];
for &item in list.iter(){
if item > largest {
largest = item;
}
}
largest
}
fn main() { //主函数
let number_list = vec![34, 50, 25, 100, 65]; //分别调用两个函数
let result = largest(&number_list);
println!("The largest number is {}.", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}.", result);
}
这段代码在运行时会报错!! 错误信息:binary operation ‘>’ cannot be applied to type ‘T’。
分析:’>’ 运算符定义在 std::cmp::PartialOrd 中,我们需要在trait bound中指明PartialOrd才可以去比较任意类型T的大小。(理解:由于generics抽象了数据类型,相应的运算符也变成了抽象意义的运算,rust 编译器不知道我们想要使用的 ‘>’ 符号是什么意思,因此必须指明这个具体的方法,才可以进行运算)于是代码修改如下:
fn largest<T: PartialOrd> (list: &[T]) -> T{ //加入trait bound
--snip--
此时依然报错:cannot move out of type ‘[T]’ , a non-copy slice.
分析:在这段代码中我们使用了两个例子:i32和char。这两种数据类型都是具有固定大小的、存储在stack中的数据,支持copy操作。但是当使用generic抽象之后,largest函数理论上要服务于所有可能的数据类型,就包括了像String这样存储在heap上、不支持copy操作的数据,因而在不声明清楚地情况下,编译器报错:一个非copy的切片,不能move!
解决:在trait bound中加入Copy要求,使用符号 ‘+’
fn largest<T: PartialOrd + Copy> (list: &[T]) -> T{ //加入trait bound
--snip--
此时,只有能够实现copy的数据类型才可以调用这段代码,也就解决了这个问题!
另一个解决方法是,修改函数的定义,使其不返回值,只返回一个指针(reference),这样即使不支持copy的数据类型也没有关系了:
fn largest_ref<T: PartialOrd> (list: &[T]) -> &T { //注意返回的数据类型,变成了reference
let mut largest = &list[0];
for item in list.iter(){
if item > largest{
largest = &item;
}
}
largest
}
此处引用的问题依然存疑,再思考!!!
book中的这句话很好,记下来:
Traits and trait bounds let us write code that uses generic type parameters to reduce duplication but also specify to the compiler that we want the generic type to have particular behavior.
lifetime多数时间是rust自己推测的,再不出现歧义的情况下不需要声明,像数据类型一样。但是当有歧义出现时,rust无法自己确定lifetime,则需要显示声明。
lifetime最主要的功能是避免野指针的出现。具体来说,就是不能出现一下情况:
The subject of the reference doesn’t live as long as the reference. 即引用的目标不能比引用本身“活的短”。(这个很好理解,如果你要引用的对象已经invalid了,那你去引用什么呢?)
lifetime的符号:'
,一般为小写,一般比较短
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
下面这段代码给出了lifetime的使用方法,声明变量x,y以及输出具有相同的lifetime
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
总之,lifetime syntax将各个变量的生命周期关联起来,以及协助函数返回值。一旦这些值联系起来,rust就获得了足够的信息可以安全的处理内存,不会出现野指针,破坏内存安全。
最早期的rust要求程序媛给出所有引用的lifetime,后来开发者发现这样会增加冗余代码,并且很多时候lifetime可以推测。所以一些有规律的lifetime被programmed into rust,成为lifetime elision rules。编译器只会对百分百确定的ref推测它的lifetime,有不确定就会抛出错误,我们需要显式声明lifetime。
函数或方法的参数的lifetime称为input lifetimes,return值的lifetime 称为 output lifetime。
编译器推测lifetime的三规则:
&self
,&mut self
,则self的lifetime会被赋值给所有的输出变量。(这种情况必须是method,因为只有method才有self,function没有)'static
所有的String literals具有静态生命周期。
这段代码综合了generic、traits和lifetime
use std::fmt::Display;
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
}
}
generic帮助我们有效地减少代码重复,Traits and trait bounds确保了即便类型是抽象generic的,他们依然具有代码所要求的相同的behavior。而lifetime则保证了不会出现悬浮指针。