The Rust Programming Language

Rust 编程语言笔记。

起源:The Rust Programming Language By Steve Klabnik, Carol Nichols

翻译参考:Rust 语言术语中英文对照表

错误处理

Rust 把谬误分为两类:

  1. 可复原的(recoverable):例如:文件未找到等。该类谬误能够提醒用户查错后持续运行程序
  2. 不可复原的(unrecoverable):例如:数组拜访越界等。该类谬误呈现后必须终止程序

对于可复原谬误,Rust 采纳 Result<T, E> 来解决;对于不可复原谬误,Rust 采纳 panic!() 宏(macro) 来解决。

在其余编程语言中,通常不会对谬误进行分类,而是应用 Exception 对立解决。

不可复原谬误和 panic!

有两种状况会执行 panic!

  1. 显式调用 panic!() 宏(macro)
  2. 程序呈现谬误,例如:数组拜访越界

默认状况下,Rust 会打印错误信息、解开(unwind)、清理栈内存、退出程序。通过环境变量,能够打印调用栈(calling stack),有助于更好 debug。

解开(unwind)栈内存 VS 立刻终止

默认状况下,当 panic 产生时,程序会开始解开(unwinding),Rust 会回到栈内存中,找到每个函数并清理数据。该操作须要破费大量资源。另一种代替形式是,**立刻终止(abort)**程序,清理内存。

此时,程序应用的内存会由操作系统来清理。要切换到立刻终止选项,在 Cargo.toml 文件中的 [profile.release] 区域增加 panic = 'abort';

[profile.release]panic = 'abort'

让咱们看一下显式调用 panic!() 的状况:

fn main() {      panic!("Crash and burn");}

如果运行上述程序,编译器会弹出:

$ ./testthread 'main' panicked at 'Crash and burn', test.rs:2:5note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

阐明此时程序 panicked

回溯

在 C 语言中,数组拜访越界是一种未定义的行为。因而,如果索引不非法,C 会返回内存中某处的数据,即便该处的内存不属于数组保留处的内存。这种行为称为“缓冲区溢出(buffer overread)”,会导致很多平安问题。

在 Rust 中,数组拜访越界会导致谬误。

能够调用环境变量 RUST_BACKTRACE 来显式调用栈的信息:

  • RUST_BACKTRACE=1: 打印简略信息
  • RUST_BACKTRACE=full:打印全副信息
$ RUST_BACKTRACE=1 ./testthread 'main' panicked at 'Crash and burn', test.rs:2:5stack backtrace:   0: std::panicking::begin_panic   1: test::main   2: core::ops::function::FnOnce::call_oncenote: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Backtrace 就是一个蕴含所有函数的列表。Rust 对回溯的解决和其余语言一样,从上往下读,首先找到源文件行,代表问题/导致 panic 的函数,该行下面的所有行示意该行调用的函数;该行上面的所有行代表被该行调用的函数。

可复原谬误和 Result

Result

对于可复原的谬误,Rust 提供了 Result<T, E> 枚举类型来解决这种谬误。

enum Result<T, E> {      Ok(T),      Err(E),}

能够看到,Result 中的 TE 采纳泛型(generic)定义,前者和 Ok 一起作为失常状况返回,后者和 Err 一起作为异样状况的返回。

例如:

use std::fs::File;fn main() {    let greeting_file_result = File::open("hello.txt");    let greeting_file = match greeting_file_result {        Ok(file) => file,        Err(error) => panic!("Problem opening the file: {:?}", error),    };}
  • 如果匹配 OkOk(File { fd: 3, path: "", read: true, write: false })
  • 如果匹配 ErrErr(Os { code: 2, kind: NotFound, message: "No such file or directory" })

处理错误类型

如果要进一步细化谬误的类型,例如对于读文件谬误,能够分为文件不存在或没有权限拜访文件等。那么通过嵌套 match 能够解决多种谬误的类型:

use std::fs::File;use std::io::ErrorKind;fn main() {        let greeting_file_result = File::open("hello.txt");      let greeting_file = match greeting_file_result {            Ok(file) => file,          Err(error) => match error.kind() {                ErrorKind::NotFound => match File::create("hello.txt") {                    Ok(fc) => fc,                  Err(e) => panic!("There is a problem when creating file: {:?}", e),              },              other_error => {                    panic!("There is a problem when open the file: {:?}", other_error);                   }          },      };}

ErrorKind 也是一种枚举类型,和 Result 以及 Option 不同,ErrorKind 须要应用 use 引入以后的作用域。下面代码中解决了NotFoundother_error 两个枚举值。

解包(unwrap) 和 expect

嵌套 match 的写法有些冗余(verbose),因而,Rust 还提供了 unwrapexpect 办法来解决 panic 或者 Error,这两个函数都定义在 Result 上。

use std::fs::File;fn main() {        let greeting_file_result = File::open("hello.txt").unwrap();}
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', test.rs:4:60note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
use std::fs::File;fn main() {        let greeting_file_result = File::open("hello.txt")                                                                      .expect("There is a problem when reading the file");}
thread 'main' panicked at 'There is a problem when reading the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', test.rs:5:72note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

两个办法的作用简直雷同,在 production 代码中,Rustaceans 偏向于应用 expect,因为其能够提供更多提示信息。

流传谬误和 ?运算符

流传谬误

当被调用函数体中呈现谬误时,与其在该函数中处理错误,更常见的形式是把把谬误返回给调用函数以更好控制代码的流程,这被称为流传谬误(propagating error)

use std::fs::File;use std::io::{self, Read};fn read_username_from_file() -> Result<String, io::Error> {    let username_file_result = File::open("hello.txt");    let mut username_file = match username_file_result {        Ok(file) => file,        Err(e) => return Err(e),    };    let mut username = String::new();    match username_file.read_to_string(&mut username) {        Ok(_) => Ok(username),        Err(e) => Err(e),    }}

? 运算符

下面的代码有些简短,能够应用 ? 运算符缩短流传错误代码:

use std::fs::File;use std::io::{self, Read};fn read_username_from_file() -> Result<String, io::Error> {      let mut username_file = File::open("hello.txt")?;      let mut username = String::new();      username_file.read_to_string(&mut username)?;      Ok(username);}

或者通过链式调用使下面的代码更简洁:

use std::fs::File;use std::io::{self, Read};fn read_username_from_file() -> Result<String, io::Error> {      let mut username = String::new();      File::open("hello.txt")?.read_to_string(&mut username)?;      Ok(username)}

? 运算符的作用和 match 简直雷同,区别在于:? 运算符蕴含了一个类型转化的过程,把多种谬误返回值对立转化同一种类型。该操作通过定义在 From trait 中的 from 函数来实现,该函数把一种类型转化为另一种类型。

具体来说,? 运算符把它所调用的返回谬误类型转化为以后函数定义的返回谬误类型。例如:以后函数返回咱们自定义的谬误类型 OurError,而 ? 所作用的函数返回的是 io::Error,那么 ? 会调用 from 函数把 io::Error 转化为 OurError

?的作用条件

应用 ? 运算符时须要留神:该运算符只能用于其作用值的类型和返回值类型兼容的函数。这是因为 ? 的作用在函数完结前提前返回值,相似于 match

例如:match 作用的类型是 Result,而返回的谬误类型是 Err(e)。依据 Result 的定义,这两者是兼容的。

然而:

use std::fs::File;fn main() {    let greeting_file = File::open("hello.txt")?;}

这种状况就会呈现谬误。因为 main 函数的返回类型是 (),而 File::open 的返回类型是 Result

解决该谬误有两种办法:

  1. 把函数的返回值类型改为 ? 作用值兼容的类型
  2. ? 替换为 match

main 函数的返回值

use std::error::Error;use std::fs::File;fn main() -> Result<(), Box<dyn Error>> {    let greeting_file = File::open("hello.txt")?;    Ok(())}

与 C 语言的标准统一,在 Rust 中,当 main 函数返回 Result<(), E> 时:

  • 如果返回的是 OK(()),那么 main 函数的返回值是 0
  • 如果返回的是 Err,那么 main 函数的返回值是 非零值

泛型、特质和生命周期

泛型

*泛型(generics)*用形象类型来代替某种具体类型,大大减少了代码的冗余。

函数、办法、构造体、枚举等都能够应用泛型。

定义泛型

应用泛型包含两个步骤:

  1. 应用尖括号(angle brackets)<> 包裹标识符 T<T>
  2. 在须要申明类型处应用 T

一般来说,标识符的名称能够任意选定。然而在 Rust 中,为了简略,通常应用简短且大写字母 T,示意 Type。

用于函数定义

泛型能够用于函数定义:

fn largest<T>(list: &<T>) -> &T {        let mut largest = &list[0];        for item in list {          if item > largest {                largest = item;            }      }      larest}

应用泛型时要留神潜在的谬误。例如:下面的函数找到列表中的最大值。然而编译器会在 item > largest 行报错,这是因为两者都是 &<T> 类型,但不是所有的类型都能够比拟。

用于构造体

泛型能够用于构造体定义:

struct Point<T> {      x: T,      y: T,}struct Point<T, U> {        x: T,      y: U,}

第一个 Point 只应用了个类型,所以字段 xy 必须是同种类型。

第二个 Point 使得 xy 的类型既能够雷同也能够不同。

用于枚举

泛型能够用于枚举定义,例如:Option 和 Result:

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> {      fn x(&self) -> &T {            &self.x       }    }

留神:在办法上应用泛型时,须要在 impl 关键字后增加 <T>,这是为了通知 Rust 该办法应用了泛型。

也能够仅给某些类型增加办法:

impl Point<f32> {    fn distance_from_origin(&self) -> f32 {        (self.x.powi(2) + self.y.powi(2)).sqrt()    }}

下面的代码示意,只有类型为 f32Point 构造体才有 distance_from_origin 办法。

性能

与应用具体类型相比,应用泛型不会导致性能变差。

Rust 应用单态(Monomorphization) 实现这一点。单态在编译时把所有的泛型转化为具体类型。

特质

*特质(traits)*定义了某种特定类型的性能,并且能够和其余类型共享。

*特质束缚(trait bound)*定义泛型可能具备某种特定行为。

特质相似于其余编程语言的 接口(interface),然而也有着一些区别。

定义特质

应用关键字 trait 定义特质:

pub trait Summary {      fn summarize(&self) -> String;}

trait 块由函数签名组成。

实现特质

相似于办法,实现特质同样应用 impl 关键字,此外还要应用 for 关键字指明要实现的对象。

pub struct Tweet {      pub author: String,      pub content: String,      pub length: u32,}impl Summary for Tweet {        fn summarize(&self) -> String {            format!("{} {}", self.author, self.content);        }}/*// 给另一构造体实现同名 traitimpl Summary for Article {        --snip--}*/fn main() {      let tweet = Tweet {              author: String::from("Mitchell"),          content: String::from("Implementing a trait"),      };      tweet.summarize();}

留神:当且仅当其中一个 trait 或者实现 trait 的类型位于以后 crate 的作用域时,才能够在其余的 trait 中援用同名 trait,并给出不同的实现。该限度是 coherence 个性的一部分,也被称为孤儿准则(orphan rule)


测试

Rust 中的测试函数用于测试被测试代码是否依照预期运行。

测试函数体通常蕴含三局部:

  1. 设置所需的变量或者状态
  2. 运行代码并测试
  3. 判断是否为预期后果

测试函数

Rust 中的测试函数应用 test 属性。属性是对于 Rust 代码的元数据(metadata)。例如:derive 就是一种元数据。

为了把某函数变为测试函数,须要在函数签名行之上增加 #[test]。应用 cargo test运行测试。

个别在创立新我的项目时,Rust 会主动增加含有测试函数的测试模块,测试模块蕴含了测试代码的模版。

例如:

#[cfg(test)]mod tests {        #[test]      fn it_works() {            let result = 2 + 2;          assert_eq!(result, 4);      }}

assert! 宏

assert! 宏由规范库提供,它用于评估布尔值。如果评估后果为 true,程序失常运行;否则,assert! 宏调用 panic! 宏导致测试失败。

#[derive(Debug)]struct Rectangle {    width: u32,    height: u32,}impl Rectangle {    fn can_hold(&self, other: &Rectangle) -> bool {        self.width > other.width && self.height > other.height    }}#[cfg(test)]mod tests {    use super::*;    #[test]    fn larger_can_hold_smaller() {        let larger = Rectangle {            width: 8,            height: 7,        };        let smaller = Rectangle {            width: 5,            height: 1,        };        assert!(larger.can_hold(&smaller));    }}

assert\_eq!, assert\_ne!

assert_eq!, assert_ne! 别离测试参数相等或者不等。

pub fn add_two(a: i32) -> i32 {    a + 2}#[cfg(test)]mod tests {    use super::*;    #[test]    fn it_adds_two() {        assert_eq!(4, add_two(2));    }}

在 Rust 中,assert_eq! 的参数为 leftright,如果两者相等,那么测试通过;否则测试失败。assert_ne! 则正好相同。

参数 leftright 表明参数的程序不重要。而在其余编程语言中,测试相等性的函数通常有着严格的程序,例如:参数别离为 expectactual,那么第一个参数只能是预期值,第二个参数是测试值。

在底层实现中,assert_eq!, assert_ne! 别离应用了 ==!= 运算符。

增加个性化错误信息

能够在 assert!, assert_eq!, assert_ne! 中把个性化错误信息作为可选参数传入,使得用户交互更加敌对。

#[test]    fn greeting_contains_name() {        let result = greeting("Carol");        assert!(            result.contains("Carol"),            "Greeting did not contain name, value was `{}`",            result        );    }

should\_panic

should_panic 是一种属性,用于测试函数体中的内容是否 panic,如果 panic 则测试通过。

pub struct Guess {    value: i32,}impl Guess {    pub fn new(value: i32) -> Guess {        if value < 1 || value > 100 {            panic!("Guess value must be between 1 and 100, got {}.", value);        }        Guess { value }    }}#[cfg(test)]mod tests {    use super::*;    #[test]    #[should_panic]    fn greater_than_100() {        Guess::new(200);    }}

Result\<T, E>

应用 Result<T, E> 来编写测试:

#[cfg(test)]mod tests {    #[test]    fn it_works() -> Result<(), String> {        if 2 + 2 == 4 {            Ok(())        } else {            Err(String::from("two plus two does not equal four"))        }    }}

在应用了 Result<T, E> 的测试中不能应用 #[should_panic]

管制测试的运行

通过给 cargo test 增加参数来管制测试的运行。

并行还是间断

在默认状况下,Rust 的测试是并行(parallel)的,这意味着测试的速度会更快,然而须要测试之间互不依赖

如果要改为间断执行,通过增加 \`\`--test-threads=1flag 来示意心愿应用1\` 个线程来运行测试:

cargo test -- --test-threads=1

显示输入后果

在默认状况下,Rust 只会显示测试失败的用例。

通过增加 --show-output flag 来额定显示测试通过的用例:

cargo test -- --show-output

按名称运行子测试

有些时候只须要运行局部测试,能够通过具体指定测试的名称来局部执行测试,假如有三个测试:

#[cfg(test)]mod tests {      use super::*;        #[test]      fn add_one() { /* --snip */ };      #[test]      fn add_two() { /* --snip */ };      #[test]      fn add_three() { /* --snip */ };}
  • cargo test 命令运行全副测试
  • carge test add_one 只运行 add\_one() 测试函数
  • cargo test add 运行所有名称蕴含 add 的函数,在本例中,运行全副函数

疏忽某些测试

通过增加 #[ignore] 属性来疏忽某些测试。

运行 cargo test 命令后,被疏忽的测试函数不会进行测试。

#[test]#[ignore]fn ingored_test() { /* --snip-- */ }

组织测试代码

Rust 中次要有两种测试形式:

  1. 单元测试(unit test):一次独立测试一个模块
  2. 集成测试(integration test):作为内部库测试代码

单元测试

单元测试的常规是:在每个文件中创立 tests 模块,该模块蕴含所有测试函数,以 cfg(test) 标识。

标识符 #[cfg(test)] 示意只有当 cargo test 时才运行测试,在 cargo build 时并不运行测试。这样的设计能够节约编译工夫。cfg 的意思是 configuration

公有函数

单元测试能够测试公有函数。

pub fn add_two(a: i32) -> i32 {    internal_adder(a, 2)}fn internal_adder(a: i32, b: i32) -> i32 {    a + b}#[cfg(test)]mod tests {    use super::*;    #[test]    fn internal() {        assert_eq!(4, internal_adder(2, 2));    }}

集成测试

对于本地代码来说,集成测试作为内部库的模式,因而只能用于测试私有函数。

能够这样组织集成测试的目录:

├── Cargo.lock├── Cargo.toml├── src│   └── lib.rs└── tests    ├── common    │   └── mod.rs    └── integration_test.rs

这是旧标准的命名形式,这样命名使得 Rust 得悉 common 中的 mod.rs 并不作为集成测试的一部分。

咱们能够把要测试的函数都写在 common/mod.rs 中,在 integration_test.rs 中发展具体测试。

集成测试只针对库 crate,如果代码中只蕴含二进制 crate,那么不能应用集成测试。