更好的错误处理无论您使用何种编程语言,都会发生错误。在 Rust 中,错误分为两个阵营:不可恢复的错误(坏的种类)和可恢复的错误(不太坏的种类)。
不可恢复的错误
Rust panic! 函数类似于 C 的 assert 宏。它生成输出来帮助用户调试问题(并在发生更多灾难事件前停止执行)。panic! 函数如 所示,它的可执行输出包含在注解中。
清单 4. 在 Rust 中使用 panic! 处理无法恢复的错误1
2
3
4
5
6
| fn main() {
panic!("Bad things happening.");
}
// thread 'main' panicked at 'Bad things happening.', panic.rs:2:4
// note: 使用 RUST_BACKTRACE=1 运行,以便实现反向跟踪。
|
从输出可以看到,Rust 运行时准确指出了发生问题的位置(第 2 行),而且发出了所提供的消息(它可以发出更多描述性信息)。输出消息表明,您可以使用名为 RUST_BACKTRACE 的特殊环境变量来运行,以便生成一个堆栈反向跟踪。也可以基于可检测的错误(比如访问一个矢量的无效索引)在内部调用 panic!。
可恢复的错误
处理可恢复的错误是编程过程的一个标准部分,Rust 包含一个很好的错误检查特性(参见 )。让我们在文件操作的上下文中查看此特性。File:pen 函数返回一种 Result<T, E> 类型,其中 T 和 E 表示通用的类型参数(在本上下文中,它们表示 std::fs::File 和 std::io::Error)。所以,当您调用 File:pen 而且未发生错误(E 为 Ok)时,T 将表示返回类型 (std::fs::File)。如果发生错误,E 表示发生的错误的类型(使用类型 std::io::Error)。(请注意,我的文件变量 _f 使用一个下划线 [_] 来省略编译器生成的无用变量警告。)
然后,我使用了 Rust 中一个名为 match 的特殊特性,该特性类似于 C 中的 switch 语句,但更强大。在本上下文中,我将 _f 与可能的错误值(Ok 和 Err)进行匹配。对于 Ok,我返回了要分配的文件;对于 Err,我使用了 panic!。
清单 5. 在 Rust 中使用 Result<T, E> 处理可恢复的错误1
2
3
4
5
6
7
8
9
10
11
12
13
14
| use std::fs::File;
fn main() {
let _f = File:pen("file.txt");
let _f = match _f {
Ok(file) => file,
Err(why) => panic!("Error opening the file {:?}", why),
};
}
// thread 'main' panicked at 'Error opening the file Error { repr: Os
// { code: 2, message: "No such file or directory" } }', recover.rs:8:23
// note: 使用 RUST_BACKTRACE=1 运行,以便实现反向跟踪。
|
Rust 中可使用 Result 枚举类型来简化可恢复的错误;还可以通过使用 match 进一步简化它们。另请注意,本示例中缺少一个 File::close 操作:该文件已在 _f 范围结束时自动关闭。
对并发性和线程的支持并发性通常会带来问题(数据争用和死锁只是其中两种)。Rust 使用原生操作系统来大量生成线程,但也会尝试减轻线程的负面影响。Rust 包含消息传递功能,允许线程相互通信(通过 send 和 recv,并通过互斥来实现锁定)。Rust 还允许一个线程借用一个值,这会为该线程提供该值的所有权并实际将值的范围(及其所有权)转交给新线程。因此,Rust 提供了内存安全和没有数据争用的并发性。
考虑 Rust 中的一个简单的线程示例,该示例介绍了一些新元素(矢量操作)并回顾了前面讨论的一些概念(模式匹配)。在 中,我首先将 thread 和 Duration 名称空间导入我的程序中。然后,我声明了一个名为 my_thread 的新函数,该函数表示我稍后将创建的线程。在这个线程中,我仅发出该线程的标识符,然后睡眠较短时间,让调度程序允许运行另一个线程。
我的 main 函数是这个示例的核心。我首先创建一个空的可变矢量,我可以使用它存储相同类型的值。然后使用 spawn 函数创建 10 个线程,并将结果连接句柄推送到该矢量中(稍后会更详细地介绍)。这个 spawn 示例与当前线程分离,这使该线程能生存到父线程退出之后。从父线程发出一条短消息后,我最后迭代了 JoinHandle 类型的矢量,并等待每个子线程退出。对于矢量中的每个 JoinHandle,我调用了 join 函数,该函数等待该线程退出后再继续处理。如果 join 函数返回一个错误,我会通过 match 调用来公开该错误。
清单 6. Rust 中的线程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| use std::thread;
use std:uration;
fn my_thread() {
println!("Thread {:?} is running", std::thread::current().id());
thread::sleep(Duration::from_millis(1));
}
fn main() {
let mut v = vec![];
for _i in 1..10 {
v.push( thread::spawn(|| { my_thread(); } ) );
}
println!("main() waiting.");
for child in v {
match child.join() {
Ok(_) => (),
Err(why) => println!("Join failure {:?}", why),
};
}
}
|
在执行时,我看到了 中提供的输出。在这里可以注意到,主线程继续执行到连接过程开始。然后,这些线程在不同的时间执行和退出,并标识线程的异步性质。
清单 7. 来自清单 6 中的示例代码的线程输出1
2
3
4
5
6
7
8
9
10
| main() waiting.
Thread ThreadId(7) is running
Thread ThreadId(9) is running
Thread ThreadId(8) is running
Thread ThreadId(6) is running
Thread ThreadId(5) is running
Thread ThreadId(4) is running
Thread ThreadId(3) is running
Thread ThreadId(2) is running
Thread ThreadId(1) is running
|
对复杂数据类型(集合)的支持Rust 标准库包含多种可在开发中使用的、流行的、有用的数据结构,包括 4 种类型的数据结构:序列、映射、集和一种附加类型。
对于序列,可以使用矢量类型 (Vec),我在线程示例中就使用了它。此类型提供一个可动态调整的数组,对收集数据供以后处理很有用。VecDeque 结构类似于 Vec,但您可以将它插入到序列的两端。LinkedList 结构也类似于 Vec,但通过它,您可以拆分和附加列表。
对于映射,为您提供了 HashMap 和 BTreeMap 结构。可以使用 HashMap 结构创建键值对,可以按照它们的键来引用元素(用于检索值)。BTreeMap 类似于 HashMap,但它可以对键进行排序,而且您可以轻松迭代所有条目。
对于集,为您提供了 HashSet 和 BTreeSet 结构(您会在映射结构后注意到它)。这些结构在没有值(仅有键)时很有用,而且可以轻松地撤销已插入的键。
最后,附加结构目前为 BinaryHeap。此结构是一个包含二进制堆的优先级队列。 |