Rust | 02. 包管理、集合(String、Vector、HashMap)、错误处理
240108 Package, Crate, Module
- 代码组织需要包括哪些细节可以暴露,哪些细节可以私有,作用域内哪些名称有效等等;Rust的代码组织(也可以叫模块系统)如下:
- Package(包),是Cargo的特性,让你构建、测试、共享crate
- Crate(单元包),一个模块树,它可以产生一个library或可执行文件
- Module(模块)、use,让你控制代码的组织、作用域、私有路径
- Path(路径),是为Struct、function、module等项命名的方式
Package和Crate
-
Crate有两种类型,即binary或library;Crate root是指源代码文件,Rust编译器会从这里开始,组成你Crate的根Module;一个Package包含了一个Cargo.toml文件(用于描述了如何构建这些Crates),一个package只能包含0-1个library crate,以及任意数量的binary crates,另外,一个library至少包含一个crate(library或binary)
-
通常而言,src/main.rs是binary crate的crate root,且crate名与package名相同;而在library crate中,则会生成一个叫做src/lib.rs的文件,其是该library crate的crate root,此时crate的名字也与package名相同;一个package中也可以同时这两个文件,此时该package中包含两个crate,一个binary一个library,都与package名相同;一个package可以有多个binary crate,文件放在src/bin目录下,每个文件是一个单独的binary crate
-
Crate的作用:将相关功能组合到一个作用域内,便于在项目间共享,防止冲突;例如rand crate,就可以使用它的名字访问其相关功能
-
定义Module可以控制作用域和私有性:在一个crate内,将代码进行分组,增强可读性,易于复用,可以使用public、private关键字控制项目(item)的私有性;使用mod关键字建立module,其可以嵌套,类似于如下的实例
1 2 3 4 5 6 7 8 9 10 11 12
mod front_of_house{ mod hosting{ fn add_to_waiting(){ } fn seat_at_table(){ } } mod serving{ fn take_order(){ } fn serve_order(){ } fn take_payment(){ } } }
路径 path
-
路径用于在rust中找到某个模块,有两种形式:
- 绝对路径,从crate root开始,使用crate名或字面值"crate"
- 相对路径,从当前模块开始,使用self、super或当前模块标识符
-
路径至少由一个标识符组成,标识符之间用::分隔,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/lib.rs mod front_of_house{ // 需要声明公开,否则无法外部访问 pub mod hosting{ pub fn add_to_waiting(){ } } } pub fn eat_at_restaurant(){ // 使用绝对路径,从顶部crate起始,逐级向下(使用crate字面量) crate::front_of_house::hosting::add_to_waiting(); // 相对路径,因为和顶级模块在同一级,可从模块名开始调用 front_of_house::hosting::add_to_waiting(); }
-
PS:定义的位置和使用的位置始终一起移动时,可以选择使用相对路径;但为了避免错误,有经验的程序员会直接写成绝对路径
私有边界和pub关键字
-
模块不仅仅可以组织代码,还可以定义私有边界;Rust中的所有条目(函数、方法、struct、模块、常量)都是私有的;父级模块无法访问子模块中的私有条目,而在子模块中可以使用所有祖先模块中的条目
-
使用pub关键字可以使模块对外暴露;根级模块可以相互调用,无论共有还是私有
-
super关键字用于访问父级模块中的内容,相当于文件系统中的两个点..
1 2 3 4 5 6 7 8
fn serve_order(){ } mod back_of_house{ fn fix_incorrect_order(){ cook_order(); super::serve_order(); //相对路径 } fn cook_order(){ } }
-
当将pub关键字放在struct前,struct就是公共的,但字段如果没有加pub修饰时,字段仍旧是私有的;即是说,struct的字段需要单独设置pub来变成共有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
mod back_of_house { pub struct Breakfast { pub toast: String, seasonal_fruit: String, } impl Breakfast { pub fn summer(toast: &str) -> Breakfast { Breakfast{ toast: String::from(toast), seasonal_fruit: String::from("peaches"), } } } } pub fn eat_at_restaurant(){ let mut meal = back_of_house::Breakfast::summer("Rye"); // 使用关联函数初始化 meal.toast = String::from("Wheat"); //pub公共字段可以修改 println!("I'd like {} toast please", meal.toast); meal.seasonal_fruit = String::from("Blueberries"); // 这一句会报错,因为season_fruit是私有的 }
-
使用pub关键字在enum前,可以把枚举类型变为公共类型,需要注意的是,当枚举类型为公共时,其变体默认也为公共类型(只有公共的才有用)
use关键字
-
可以使用use关键字将路径导入到当前作用域内(类似Linux的符号链接),但仍旧遵循私有原则
1 2 3 4 5 6 7 8 9 10 11 12 13 14
mod front_of_house { pub mod hosting { pub fn add_to_waitlist(){ } } } use crate::front_of_house::hosting; // 使用绝对路径 use front_of_house::hosting; // use也可以使用相对路径 pub fn eat_at_restaurant(){ hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); }
-
在上面的案例中,有经验的程序员在引入一个函数时,是引入到函数的上一级,而不是直接到函数名(
crate::front_of_house::hosting::add_to_waitlist
),这是想要强调,该函数是在hosting模块下的,如果直接引入函数名,则会与本地定义函数混淆、无法区分 -
但对于引入struct、enum类型时,可以直接指定完整路径到其本身;但对于同名条目,还是需要指定到父级,否则会混淆
1 2 3 4 5
use std::collections::HashMap; fn main(){ let mut map = HushMap::new(); map.insert(1, 2); }
1 2 3 4 5
use std::fmt; use std::io; fn f1() -> fmt::Result { } fn f2() -> io::Result{ } fn main() { }
-
as关键字可以为引入的路径指定一个本地的别名
1 2 3 4 5
use std::fmt::Result; use std::io::Result as IoResult; fn f1() -> Result { } fn f2() -> IoResult { } fn main() { }
-
使用use将路径(名称)导入到作用域后,该名称在此作用域内是私有的。例如下面的代码,函数
eat_at_restaurant()
对于外部代码而言是公开的;但名称hosting却对外面是默认私有的,需要在use语句前使用pub关键字,将其公开1 2 3 4 5 6 7 8 9 10 11 12 13
mod front_of_house { pub mod hosting { pub fn add_to_waitlist(){ } } } pub use crate::front_of_house::hosting; pub fn eat_at_restaurant(){ hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); }
-
小结一下,pub use的作用就是重导出,其不仅仅将条目引入当前作用域,也使该条目可以被外部代码引入到他们的作用域内
-
可以使用嵌套语句,来在同一行内同时引入相同包/模块下的多个条目,其格式为:
use 路径相同部分::{路径, 不同, 部分} 1 2 3 4
use std::cmp::Ordering; use std::io; use std::{cmp::Ordering, io};
1 2 3 4
use std::io; use std::io::Write; use std::io::{self, Write};
-
可以使用通配符*,可以将路径中所有公共条目都引入到当前作用域内;需要注意的是,通配符需要谨慎使用,其仅仅在测试阶段(将所有需要测试代码引入到tests模块)或预导入模块(prelude)
1
use std::collections::*;
外部包(package)
-
在
Cargo.toml
文件中添加外部包的名字和版本号,cargo build时就会自动从https://crates.io/下载到本地环境,随后便可使用use将特定条目引入到作用域 -
可以在
\Users\user\.cargo\config
目录下写配置文件来更换清华源1 2 3 4 5 6 7 8 9
[source.crates-io] registry "https://github.com/rust-lang/crates.io-index" replace-with 'tuna' [source.tuna] registry "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git" [net] git-fetch-with-cli true
-
Rust的标准库std也被当做是外部包,需要使用use引入特定条目到当前作用域,但因其本身写入到rust语言本体之中,不需要修改
Cargo.toml
文件来包含std
模块拆分到不同文件
-
定义模块时,模块名后可以直接写分号
;
,而不是代码块,此时Rust会从与模块同名的文件中加载内容(即实现了将模块拆分到不同的文件内)1 2 3 4 5 6 7 8
src ├─ lib.rs │ mod front_of_house{ │ pub mod hosting;{ │ pub fn add_to_waitlist(){ } │ } │ } └─ main.rs
1 2 3 4 5 6 7 8
src ├─ front_of_house.rs │ pub mod hosting;{ │ pub fn add_to_waitlist(){ } │ } ├─ lib.rs │ mod front_of_house; └─ main.rs
1 2 3 4 5 6 7 8 9
src ├─ lib.rs │ mod front_of_house; ├─ main.rs ├─ front_of_house.rs │ mod hosting; └─ front_of_house └─ hosting.rs pub fn add_to_waitlist(){ }
240109 常用集合
Vector
-
可以使用Vector类型存储 类型相同的多个值,写作Vec<T>,其由标准库提供
1 2 3 4
fn main(){ let v: Vec<i32> = Vec::new(); // 因为初始化时为空,编译器无法推断类型,需要手动标注i32类型 }
1 2 3 4
fn main(){ let v = vec![1, 2, 3]; // 使用宏vec!直接初始化,此时不需要显式指定类型 }
-
使用
push()
函数向其中添加元素1 2 3 4 5
fn main(){ let v = Vec::new(); v.push(1); // 由于向其中添加了值,此时编译器可以推断类型 v.push(2); }
-
使用索引或
get()
方法获得其中的元素,索引从0开始,超出索引范围后会引发程序崩溃,而get方法返回Option枚举类型,需要手动处理1 2 3 4 5 6 7 8 9 10
fn main(){ let v = vec![1,2,3,4,5]; let third: &i32 = &v[2]; println!("The third element is {}", third); match v.get(2) { Some(third) => println!("The third element is {}", third), None => println!("There is no third elemrnt"), } }
-
所有权和借用规则要求,不能在同一作用域内同时拥有可变和不可变引用,该规则仍旧适合vector;因为Vector的其元素成集合统一存储在heap上,因而不能只考虑一个元素是否可变,而需要关注整个vector是否可变
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
fn main(){ let mut v = vec![1,2,3,4,5]; let first = &v[0]; v.push(6); // 因为前一行定义为不可变引用,这一行会报错 println!("The first element is {}", first); // 需要修改时,需要界定为可变引用 for i in &mut v{ *i += 50; } for i in v{ println!("{}", i); } }
-
在Vector中只能存放相同类型的数据,当有需求存储不同类型数据时,可以基于枚举的如下特性,利用附加数据的枚举类型来完成
-
枚举的变体可以附加不同类型的数据
-
枚举变体的定义在同一个枚举类型下
-
PS:在使用该方法定义Vector时,需要知道详尽的数据类型,不能存在类型未知的情况;Trait对象对于解决该问题有帮助
1 2 3 4 5 6 7 8 9 10 11 12 13
enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } fn main(){ let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(3.5), ] }
-
字符串 String
-
字符串是Byte的集合,能够提供一些方法将byte解析为文本的类型,在Rust中,通常意义上所说的字符串是指String类型或&str类型;此外,标准库中也还提供了许多其他的字符串类型,如OsString、OsStr、CString、CStr等
-
据说Rust的字符串很劝退,原因可能在于字符串本身的复杂性、Rust倾向于暴露可能的错误、Rust使用utf-8编码
-
在Rust的核心语法层面,只有一个字符串类型,是字符串切片(str或&str),其是对储存在其他地方、utf-8编码的字符串的引用
-
而字符串String是来自标准库,是可增长、可修改、可拥有的类型;由于其本身就是Byte的集合,因而许多Vec<T>的操作也都可用于String,可以使用String::new()创建字符串,也可以直接使用
to_string()
方法(该方法可用于实现了Display trait的类型,包含字符串字面值)或String::from()
函数创建初始化一个字符串1 2 3 4 5 6
fn main(){ let data = "initial contents ⭐"; let s1 = data.to_string(); let s2 = String::from("initial contents"); }
-
String的大小是可以增减的,其内容也可以更改
-
使用
push_str()
方法可以向字符串尾部添加内容,该方法的参数是字符串切片(因而可以直接填写字面值),因为是传入引用,是借用字符串,不获取其所有权,因而修改后的字符串仍旧可以正常使用 -
使用
push()
方法,可以把单个字符添加到字符串尾部1 2 3 4 5 6
fn main(){ let mut s = String::from("foo"); s.push_str("bar"); s.push('x'); println!("{}", s); }
-
使用
+
运算符直接拼接字符串,其相当于使用了类似签名fn add(self, s:&str) -> String {...}
这样的方法(实际上是一个泛型,会更复杂),因而需要加号前为字符串,加号后为字符串切片,在相加后前面的变量所有权移交到add内部,离开后会失效,而后面的变量则可以继续保留所有权而可以继续使用1 2 3 4 5 6 7
fn main(){ let s1 = String::from("Hello, "); let s2 = String::from("World!"); let s3 = s1 + &s2; println!("{}", s3); }
PS:在该例子中,加号后面使用
&s2
是字符串引用类型,并不是上面要求的字符串切片,这是因为Rust存在一个解引用强制转换(deref coercion)的机制 -
也可以使用
format!
宏来完成字符串的拼接,其更灵活也不会取得任何参数的所有权1 2 3 4 5 6 7 8
fn main(){ let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{}-{}-{}", s1, s2, s3); println!("{}", s); }
-
-
按照索引语法访问String的某部分时会报错,即是说Rust字符串不支持索引语法访问;实际上,String是对Vec<u8>的包装,可以使用
len()
方法返回其字节长度1 2 3 4 5 6 7
fn main(){ let len1 = String::from("abcdefg").len(); let len2 = String::from("æɑðɜɣʝʪ").len(); println!("{}, {}", len1, len2); // 输出为7, 14 }
之所以输出24,是因为里面的每个字母占用了2字节;这里所谓的字母称之为Unicode标量值,即是说,rust中不是所有的字节索引都能对应到一个有效的Unicode标量值,因而Rust在开发早期就为了杜绝此类错误,直接拒绝了使用索引值访问
-
Rust有三种看待字符串的方式:字节Bytes、标量值Scalar values和字形簇Grapheme cluster;其中字形簇才是最接近我们所理解的“字母”;可以使用
bytes()
、chars()
方法获得其字节和标量值的迭代器,用于遍历字符串;对于字形簇的获取,其很复杂,标准库为提供1 2 3 4 5 6 7 8 9 10 11
fn main(){ let s = "æɑðɜɣʝʪ"; for b in s.bytes(){ println!("{}", b); } for b in s.chars(){ println!("{}", b); } }
-
此外,Rust不允许对String进行索引,还因为每次索引操作都消耗一个常量时间O(l),而String无法保证这个时间,因为要确认这个时间需要遍历整个字符串来计算有多少个合法字符
-
可以使用[]和一个范围来创建字符串的切片(切割字符串),但是需要谨慎使用,如果切割时跨越了字符边界,程序就会panic
1 2 3 4 5
fn main(){ let s = "æɑðɜɣʝʪ"; let s1 = &s[0..4]; //可以切割出两个字符 let s2 = &s[0..3]; //编译不会报错,运行会报错,说索引3不是一个字符的边界 }
-
小结,Rust选择将正确处理String作为程序员的默认行为,要求程序员必须在处理utf-8数据之前投入更多精力,从而防止在开发后期处理涉及非ASCII字符时的错误
HushMap
-
哈希表(HashMap)是以键值对的形式存储数据的一种方式(Heap上存储),相当于python或C#中的字典,在rust中以HashMap<K, V>来表示;其不在prelude中,需要手动导入
-
可以使用
new()
函数创建HashMap,使用insert()
方法插入键值对,如果没有数据初始化或随后插入数据的话,需要指定数据类型;HashMap是同构的,其所有的Key类型相同,所有的Value类型相同;标准库对其支持很少,没有内置的宏来创建HashMap1 2 3 4 5 6
use std::collection::HashMap; fn main() { let mut score: HashMap<String, i32> = HashMap::new(); score.insert(String::from("Blue"), 10); }
-
也可以使用
collect()
方法来创建HashMap,需要手动指定collect之后的数据类型1 2 3 4 5 6
use std::collection::HashMap; fn main() { let teams = vec![String::from("Blue"), String::from("Yellow")]; let intial_scores = vec![10,50]; let scores: HashMap<_,_> = teams.iter().zip(intial_scores.iter()).collect(); }
-
HashMap和所有权:
- 对于实现了Copy trait的类型(例如i32),值会被复制到HashMap中
- 对于拥有所有权的值(例如String),值会被移动,所有权会转移给HashMap
1 2 3 4 5 6 7 8 9 10 11 12 13
use std::collection::HashMap; fn main() { let field_name = String::from("Favorite color"); let field_value = String::from("Blue"); let mut map = HashMap::new(); map.insert(field_name, field_value); println!("{}, {}", field_name, field_value); // 所有权移交HashMap,值会失效 map.insert(&field_name, &field_value); println!("{}, {}", field_name, field_value); // 插入值的引用,值本身不会移动 // 在HashMap有效的期间,被引用的值必须保持有效 }
-
HashMap值的访问可以使用
get()
方法,其可以返回一个Option<T>
枚举类型;可以使用for循环遍历HashMap的引用,保留其所有权1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
use std::collection::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); let score = scores.get(&team_name); match score { Some(s) => println!("{}", s), None => println!("Team not exist"), }; for (k, v) in &scores { println!("{}: {}", k, v); } }
-
HashMap的更新:
- 可以直接插入(k, v),当k相同时,v会将原来值覆盖掉
- 使用entry检查指定的k是否有对应的v,其参数为k,返回值为enum Entry类型(存在时返回值的可变引用),配合
or_insert()
方法来更新值
1 2 3 4 5 6 7 8 9 10 11
use std::collection::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Blue"), 50); println!("the value of {} is {}", k, v); scores.entry(String::from("Yellow")).or_insert(50); scores.entry(String::from("Blue")).or_insert(70); println!("{:?}", scores); }
-
一个文本计数的案例
1 2 3 4 5 6 7 8 9 10
use std::collection::HashMap; fn main() { let text = "hello world wonderful world"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; //解引用 } println!("{:#?}", map); }
-
hash函数决定了如何在内存中存放K和V,标准库中的HashMap使用带加密功能的Hash函数,可以抵抗拒绝服务攻击(DoS),其不是最快的,但更安全。可以指定不同的hasher来更换Hash函数(hasher是实现BuildHasher trait的类型)
250826 错误处理
- Rust是高可靠语言,在大部分时候,在编译阶段就会提示错误,并要求处理;在Rust中,没有异常处理机制,而是使用
Result<T,E>
泛型来处理可恢复错误,使用panic!
宏处理不可恢复错误 - 通常来说,错误可以分为两大类:
- 可恢复错误,如文件未找到
- 不可恢复错误,即bug,如访问索引超出范围
panic!
和不可恢复的错误
-
panic!()
可以是自己写的,也可以是程序依赖代码调用的;使用set RUST_BACKTRACE=1 && cargo run
命令可以在执行代码的同时查看到错误的回溯信息(找到panic!的发生位置)1 2 3 4 5
fn main{ // panic!("Stop Program!"); let v = vec![1,2,3]; v[100] }
-
当
panic!
执行时,程序会做以下操作:打印错误信息、展开(unwind)并清理调用栈(stack)、退出程序;展开调用栈的操作意味着Rust沿着调用栈往回走,清理而每个遇到的函数中的数据,其工作量巨大;可以手动设置立即中止(abort)调用栈,不由程序进行清理而是直接停止程序,交由操作系统对内存进行清理,这样的配制可以让二进制文件更小;设置的方法是在Cargo.toml中修改profile设置,添加panic=‘abort’即可1 2
[profile.release] panic = 'abort'
-
panic!
的使用场景是,当你认为你可以代替调用者,判断出错误不可恢复时,才会使用;通常情况下,在定义一个可能失败的函数时,优先考虑返回Result。在建议的规范中,只有很少数情景会使用panic!,如编写实例演示概念、原型代码(如编写unwrap或expect)、使用unwrap()或expect()在测试代码中标记错误 -
有的时候,你可以获得比编译器多的信息,肯定其结果不会发生错误的时候,也可以使用unwrap()处理数据类型的转化,如下场景中,手动输入的IP地址一定是对的,但parse()方法仍旧返回Result类型,可以直接使用unwrap()获取需要的结果
1 2 3 4
use std::net::IpAddr; fn main(){ let home: IpAddr = "127.0.0.1".parse().unwrap(); }
-
当代码最终可能处于损坏状态(bad state)时,最好使用
panic!()
,此时某些假设、保证、约定或不可变性被打破,例如非法的值、矛盾的值或空缺的值被传入或如下可能的场景- 破坏状态并不是预期能够偶尔发生的事情
- 在此之后,代码处于破坏状态就无法运行了
- 在所使用的类型中,没有一个好的方法来编码这些信息 一些具体的例子:
- 调用你的代码,传入无意义的参数值:panic!
- 调用外部不可控代码,返回非法状态,你无法修复:panic!
- 如果失败是可预期的:Result
- 当你的代码对值进行操作,首先应该验证这些值的合法性,不合法时:panic!
-
为了验证值的合法性,可以创建新的类型,把验证逻辑放在实例的构造函数里;这里对之前猜数游戏的案例做部分修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
pub struct Guess{ value: i32, //私有的,只提供get不提供set,确保值只能通过new()设置,必须经过检查 } impl Guess { //关联函数new(),作为构造函数 pub fn new(value: i32) -> Guess{ if value > 1 || value > 100 { panic!("Guess value must be between 1 and 100, got {}", value); } Guess { value } } pub fn value(&self) -> i32 { self.value } } fn main() { loop { // ... let guess = "32"; let guess: i32 = match guess.trim().parse(){ Ok(num) => num, Err(_) => continue, }; let guess = Guess::new(guess); //在创建实例的时候验证,如果创建成功则说明通过验证 } }
Result枚举
其定义结构如下,有两个变体,分别绑定了一个泛型参数。可以理解为,T为操作成功情况下,Ok变体里返回的数据的类型,E为操作失败情况下,Err变体里返回的错误的类型。使用match处理Result,当需要解析错误时也可以使用match做进一步的区分。
|
|
-
例如下面的例子,File::open()返回的结果就是一个Result枚举类型,如果文件存在并读取成功,其返回的是File类型,如果不存在则返回Err;使用match处理Result,值得关注的是,Result及其变体类型,同Option一样,由prelude带入作用域,不需要额外手动标注;可以使用
unwrap()
方法来快速match不同类型并返回系统报告信息(match+panic!);也可以使用expect()
方法在解析错误的同时打印自定义的错误信息1 2 3 4 5 6 7 8 9 10
use std::fs::File; fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => { panic!("Error opening file {:?}", error); }, }; let f = File::open("hello.txt").unwrap(); let f = File::open("hello.txt").expect("无法打开文件"); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use std::fs::File; 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); } }; }
- 在本例中嵌套了好几层
match
,其代码不够直观,之后会学习闭包(closure)来优化这一段代码(调用unwrap_or_else()
)
1 2 3 4 5 6 7 8
let f = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Error creating file:{:?}", error); }) } else{ panic!("Error opening file:{:?}",error); });
- 在本例中嵌套了好几层
-
传播错误:在错误发生时,不仅仅可以在函数中处理该错误,也可以把错误返回给函数的调用者,让其解决该错误(相当于java中的
throw
),其代码逻辑如下1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
use std::fs::File; use std::io; use std::io::Read; fn read_from_file() -> Result<String, io::Error> { let f = File::open("hello.txt"); let 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_from_file(); }
-
Rust提供
?
操作符来快速解析结果并抛出异常,其效果等同于match+return;这里值得注意的是,?
运算符只可以用在返回Result类型的函数中,其在执行时,会隐式地将捕获到的错误交由from
函数进行处理和转化,将捕获到的错误类型转化为当前函数返回类型所定义的错误类型;from是Trait std::convert::From上的函数,用于错误之间的转化,只有此错误类型实现了目标类型的from函数,?
才可以将不同错误原因,返回为同一种错误类型;此外,?
还支持链式调用1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
use std::fs::File; use std::io; use std::io::Read; fn read_from_file() -> Result<String, io::Error> { let mut s = String::new(); let mut f = File::open("hello.txt")?; f.read_to_string(&mut s)?; Ok(s) } fn read_from_file() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) } fn main() -> Result<(), Box<dyn Error>>{ let f = File::open("hello.txt")?; Ok(()) }
main()函数需要标注返回值类型才可以使用
?
,Box<dyn Error>
是一个trait对象,可以理解为任何可能的错误类型