“Rust是一种采用过去的知识解决将来的问题的技术。”——Graydon Hoare
“高效开发的关键在于不断制造一些新的有趣错误。”——Tom Love
“一切皆有可能。”——Ian Hickson
“对发送的内容保守一点,对收到的内容宽容一点。”——John Postel
“如果你愿意限制方法的灵活性,那么总是会有意外的收获。”——John Carmack
“从那时起,当计算机出现任何问题时,我们都说它里面有bug。”——Grace Hopper
“算法必须名副其实。”——Donald Knuth
“不断发展的系统会增加其复杂性,除非采取措施优化它。”——Meir Lehman
“Lisp不是一种语言,它是一种建筑材料。”——Alan Kay
“你可以这么做,但是最好知道自己在做什么。”——A Rustacean
“一般而言,程序应该惜字如金。”——Kernighan and Plauger
“程序和诗差不多,你先得把它写出来。”——E. W. Dijkstra
“程序最重要的属性在于它是否满足了用户的需求”。——C. A. R. Hoare
“有时候,优雅的实现只是一个函数。不是一种方法、一个类或者一个框架,只是一个函数。”——John Carmack
“保持好奇心,广泛阅读,尝试新事物,人们所谓的智慧很多情况下都可以归结为好奇心。”——Aaron Swartz
“在编程中,关键不在于解决问题,而是决定要解决哪些问题。”——Paul Graham
“如果调试是消除bug的过程,那么编程必然是引入bug的过程。”——E. W. Dijkstra
以上这些经典语句出自哪里?
小编从《精通Rust(第2版)》里筛选出来的。这是怎么的一本书呢?
这本书是关于Rust编程语言的,它能够让你构建各种软件系统——从底层的嵌入式软件到动态的Web应用程序。Rust快速、可靠、安全,它提供了甚至超过C/C++的性能和安全保证,同时还是一种学习曲线比较平滑的热门编程语言。通过逐步完善,与积极友好的社区文化相结合,该语言的前景会非常美好。
本书内容共17章,由浅入深地讲解Rust相关的知识,涉及基础语法、软件包管理器、测试工具、类型系统、内存管理、异常处理、高级类型、并发模型、宏、外部函数接口、网络编程、HTTP、数据库、WebAssembly、GTK+框架和GDB调试等重要知识点。
本书适合想学习Rust编程的读者阅读,希望读者能够对C、C++或者Python有一些了解。书中丰富的代码示例和详细的讲解能够帮助读者快速上手,高效率掌握Rust编程。
样章赏析:截选自第3章部分内容
3.1 测试的目的
“一切皆有可能。”
——Ian Hickson
软件系统就像具有齿轮和其他部件的机器。如果任何一个齿轮发生故障,那么整个机器很有可能无法正常运转。在软件中,各个齿轮就是你所使用的功能、模块或程序库。软件系统的各个组件的功能测试是保证代码高质量、有效且实用的方法。它并不能验证代码中是否存在bug,但有助于建立开发人员将代码部署到生产环境中的信心,并在项目长期维护时保持代码的健壮性。
此外,如果没有单元测试,就很难在软件中进行大规模重构。在软件中明智而均衡地使用单元测试的好处是长远的。在代码实现阶段,编写良好的单元测试是软件组件的非正式规范。在维护阶段,现有的单元测试可以用来防止代码库的回归,从而鼓励系统立即修复问题。在Rust这样的编译语言中,由于编译器提供了有用的错误诊断信息,单元测试的回归所涉及的重构(如果有的话)会受到更多“指导”,因此效果更好。
单元测试的另一个好处在于,它鼓励程序员编写主要依赖于输入参数的模块化代码,即无状态函数。这使得程序员能够避免编写依赖于全局可变状态的代码。依赖于全局可变状态的测试很难构造,但是,单纯考虑为一段代码编写测试的行为有助于程序员在实现过程中找出一些低级的错误。对任何试图了解代码库的不同部分之间如何相互作用的新手来说,它们也是非常好的文档。
需要注意的是,测试对任何软件项目都是不可或缺的。现在,让我们看看如何在Rust中编写测试,首先从如何组织测试的结构开始。
3.2 组织测试
在开发软件时,我们通常会编写两种测试:单元测试和集成测试。它们用于不同的目的,并与被测试代码进行不同的交互。单元测试总是轻量级、单个组件的测试,开发人员可以经常运行它们,从而提供更快速的反馈循环;而集成测试比较庞大,并根据环境和规格模拟真实的应用场景。Rust内置的测试框架为我们提供了编写和组织这些测试的合理默认参数。
- 单元测试:单元测试通常编写在包含被测试代码的同一模块中。当这些测试的数量增加时,它们被组织成嵌套模块形式的一个实体。通常在当前模块中创建一个子模块,对该测试进行命名(例如根据约定将之命名为tests),并添加相应的注释属性(#[cfg(test)]),然后将所有与测试有关的函数放入其中。该属性只是告知编译器在测试模块中引用代码,但这只在执行cargo test命令时生效。稍后将详细介绍属性的相关信息。
- 集成测试:集成测试在程序库根目录下的tests/目录中单独编写。它们被构造成本身就像是被测试程序库的使用者。tests/目录中的任何.rs 文件都可以添加一个 use声明来引入需要测试的任何公共API。
要编写上述任何一种测试,我们需要先熟悉一些和测试有关的原语。
测试原语
Rust内置的测试框架基于一系列主要属性和宏组成的基元。在我们编写任何实际的测试之前,熟悉如何有效地使用它们将非常重要。
属性
Rust 代码中的属性是指元素的注释。元素项是软件包(crate)中的顶层语言结构,例如函数、模块、结构体、枚举和声明的常量,以及在软件包根目录下定义的其他内容。属性通常是编译器内置的,不过也可以由用户通过编译器插件创建。它们指示编译器为其下显示的元素注入额外的代码或含义,如果对应的是模块,那么会对该模块应用上述规则。我们将会在第 7 章详细介绍这些内容。为了简化本小节讨论的主题,我们将会讨论两种属性。
- #[<name>]:这适用于每个元素,通常显示在它们定义的上方。例如,Rust中的测试函数使用#[test]属性进行注释。它表示该函数将被视为测试工具的一部分。
- #![<name>]:这适用于每个软件包。注意,与#[<name>]相比,其中额外包含一个“!”。它通常位于用户软件包根目录的最顶端部分。
注意
如果要创建程序库项目,那么项目根目录中的文件一般是lib.rs文件,而创建二进制项目时,项目根目录中的文件将是main.rs文件。
还有其他形式的属性,例如在模块中编写测试时使用的#[cfg(test)]。此属性添加在测试模块之上,以提示编译器有条件地编译模块,但仅在测试模式下有效。
属性不仅限于作用在测试代码上,它们在Rust代码中用途广泛。后文将会介绍与之有关的更多内容。
断言宏
在测试中,当给定一个测试用例时,我们尝试在给定的输入区间内断言程序组件的预期行为。语言通常提供被称为断言函数的函数来执行这些断言。Rust为我们提供了通过宏实现的断言函数,帮助我们实现相同的功能。接下来将介绍一些常用的断言函数。
- assert!:这是最简单的断言宏,它是通过布尔值进行断言的。如果值为false,则测试失败,同时会提示产生错误的代码行。此外还可以额外添加格式化字符串,后跟相应数量的变量,用于提供自定义异常消息:
-
assert!(
true)
;
-
assert!(
a == b,
"{} was not equal to {}", a, b)
;
- assert_eq!:这会接收两个值,如果它们不相等,则会执行失败。它也可以采用自定义异常信息的格式化字符串:
-
let a =
23;
-
let b =
87;
-
assert_eq!(a, b,
"{} and {} are not equal", a, b);
- assert_ne!:这与assert_eq!类似,因为它需要接收两个值,但只有在两个值互不相等的情况下才进行断言。
- debug_assert!:这类似于assert!。debug断言宏也可以用在除测试代码之外的代码中。在其他代码中,这主要用于代码运行时,对应该保存的任何契约或不变性进行断言的情况。这些断言仅在调试版本中有效,并且有助于在调试模式下运行代码时捕获断言异常。当代码以优化模式编译时,这些宏调用将被忽略,并被优化为无操作。它还有类似的变体,例如debug_assert_eq!和debug_assert_ne!,它们的工作方式类似assert!宏。
为了比较这些断言宏中的值,Rust需要依赖特征。例如,“assert!(a == b)”中的“==”,实际上会转变成一个方法调用,即a.eq(&b),eq方法来自特征PartialEq。Rust中的大多数内置类型都实现了PartialEq和Eq特征,因此可以对它们进行比较。在第4章中将讨论这些特征的细节,以及PartialEq和Eq之间的区别。
但是,对于用户自定义类型,我们需要实现这些特征。幸运的是,Rust为我们提供了一个名为derive的简便宏,它可以根据名称实现一个或多个特征。可以通过将#[derive(Eq, PartialEq)]注释放在任何用户自定义类型上来使用它,但要注意括号内的特征名称。derive是一个过程宏,它只是简单地为实现它的类型的impl块生成代码,并实现特征方法或任何关联函数。第9章我们将详细讨论这些宏。
接下来,让我们开始编写一些测试。
3.3 单元测试
通常,一个单元测试就是一个函数,它实例化应用程序的一小部分,并独立于代码库的其他部分验证其行为。在Rust中,单元测试通常是在模块中编写的。理想情况下,它们应该仅用于涵盖模块的功能及其接口。
3.3.1 第一个单元测试
以下是我们的第一个单元测试:
-
// first_unit_test.rs
-
-
#[test]
-
fn basic_test() {
-
assert!(
true);
-
}
一个单元测试会被构造成一个函数,并使用[test]属性进行标记。前面的basic_test函数中并没有什么复杂的内容。其中有一个基本的断言assert!,将true值作为参数。为了更好地有序组织代码,你还可以创建一个名为tests(根据约定)的子模块,并将所有相关的测试代码放入其中。
3.3.2 运行测试
我们运行此测试的方法是在测试模式下编译代码。编译器会忽略带有测试标记的函数的编译,除非它被告知在测试模式下运行。这可以通过在编译测试代码时将--test标记参数传递给rustc实现。之后,只需执行编译后的二进制文件即可运行测试。对于之前的测试,我们将在测试模式下运行以下命令来编译它:
rustc --test first_unit_test.rs
通过--test标记参数,rustc将main函数和一些测试工具代码放在一起,并将所有已定义的测试函数作为线程并行调用。默认情况下,所有测试都是并行运行的,除非将下列环境变量设置成“RUST_TEST_THREADS=1”。这意味着如果我们希望在单线程模式下运行之前的测试,那么可以通过“RUST_TEST_THREADS=1”来实现。
现在Cargo已经支持运行测试,所有这些通常都是通过调用cargo test命令在内部完成的。此命令为我们编译并运行测试已标记的函数。在接下来的示例中,我们将主要使用Cargo来执行测试。
3.3.3 隔离测试代码
当我们的测试变得日益复杂时,可能需要创建其他辅助方法,这些方法只能在测试代码的上下文中使用。在这种情况下,将相关的测试代码与实际代码隔离是很有益的。我们可以通过将所有与测试有关的代码封装在模块中,并在其上放置#[cfg(test)]注释标记来达到此目的。
#[cfg(...)]属性中的 cfg 通常用于条件编译,但不限于测试代码。它可以为不同体系结构或配置标记引用或排除某些代码。这里的配置标记是test。你可能还记得第2章的测试中已经采用了这种格式。这样做的好处是,只有当你运行cargo test命令时,测试代码才会被编译,并包含到已编译的二进制文件中,否则其将会被忽略。
假如你希望以编程方式生成测试数据,但是不必在正式上线的版本中包含这些代码。让我们通过运行cargo new unit_test --lib命令来演示这一点。在lib.rs中,我们定义了一些测试和函数:
-
// unit_test/src/lib.rs
-
-
//我们想要测试的函数
-
fn sum(a:
i8, b:
i8) ->
i8 {
-
a + b
-
}
-
-
#[cfg(test)]
-
mod tests {
-
fn sum_inputs_outputs() ->
Vec<((
i8,
i8),
i8)> {
-
vec![((
1,
1),
2), ((
0,
0),
0), ((
2, -
2),
0)]
-
}
-
-
#[test]
-
fn test_sums() {
-
for (input, output)
in sum_inputs_outputs() {
-
assert_eq!(crate::sum(input.
0, input.
1), output);
-
}
-
}
-
}
我们可以通过cargo test命令来运行这些测试。让我们详细解读一下上述代码。在sum_inputs_outputs函数中会生成已知的输入和输出对。#[test]属性使得test_sums函数不会出现在正式发布的编译版本中。但是,sum_inputs_outputs并没有使用#[test]进行标记,如果它是在tests模块之外声明的,那么它会被包含到正式发布的编译版本中。通过将#[cfg(test)]标记和一个mod tests子模块搭配使用,并将所有测试代码及其相关函数封装到此模块中,可以确保代码和生成的二进制文件都是纯粹的测试代码。
我们的sum函数在前面没有pub关键字修饰的情况下是私有的,这意味着模块中的单元测试还允许用户测试私有的函数和方法。这样做会非常方便。
3.3.4 故障测试
还有一些测试用例,用户希望API方法基于某些输入而执行失败,并且希望测试框架断言此失败。Rust为此提供了一个名为#[should_panic]的属性。下面是一个使用此属性的测试:
-
// panic_test.rs
-
-
#[test]
-
#[should_panic]
-
fn this_panics() {
-
assert_eq!(
1,
2);
-
}
#[should_panic]属性可以和#[test]属性搭配使用,以表示运行this_panics函数应该导致不可恢复的故障,在Rust中这类异常被称为panic。
3.3.5 忽略测试
编写测试时另一个有用的属性是#[ignore]。如果你的测试代码量非常庞大,那么可以使用#[ignore]属性标记告知测试工具在执行cargo test命令时忽略此类测试功能。然后你可以向测试工具或cargo test命令传递--ignored参数来单独运行这些测试。下面的代码包含一个笨拙的循环操作,当运行cargo test命令时,默认情况下会被忽略:
-
// silly_loop.rs
-
-
pub
fn silly_loop() {
-
for _ in
1.
.1_000_000_000 {};
-
}
-
-
#[cfg(test)]
-
mod tests {
-
#[test]
-
#[ignore]
-
pub
fn test_silly_loop() {
-
::silly_loop();
-
}
-
}
注意test_silly_loop函数上方的#[ignore]属性,下面是忽略测试后的输出结果:
注意
也可以通过向Cargo提供测试函数名称来运行单个测试,例如cargo test some_test_func。
3.4 集成测试
虽然单元测试可以测试用户的软件包和模块内部的私有接口,但是集成测试有点类似于黑盒测试,旨在从消费者的角度测试软件包公共接口端到端的使用。在编写代码方面,编写集成测试和单元测试没有太大的区别,唯一的区别是目录结构和其中的项目需要公开,开发人员已经根据软件包的设计原则公开了这些项目。
3.4.1 第一个集成测试
如前所述,Rust希望所有集成测试都在tests/目录下进行。在我们对程序库进行测试时,tests/目录中的文件会被编译成相对独立的二进制程序包。在接下来的示例中,我们将通过运行cargo new integration_test --lib命令创建一个新的程序库。和前面的单元测试一样,其中还包含相同的sum函数,不过我们现在添加了一个tests/目录,其中包含一个集成测试函数,定义如下所示:
-
// integration_test/tests/sum.rs
-
-
use
integration_test::
sum;
-
-
#[test]
-
fn sum_test() {
-
assert_eq!(sum(
6,
8),
14);
-
}
首先,将sum函数纳入作用域。其次,我们使用一个sum_test函数,它在返回值时会调用sum函数和断言函数。当我们尝试运行cargo test命令时,会出现以下错误:
这个错误似乎很合理。我们希望程序库的用户调用sum函数,但是在程序库中却默认将其定义为私有的。因此,在sum函数之前添加pub修饰符,再次运行cargo test命令后,编译顺利通过:
这里是我们的integration_test示例程序库的目录树视图:
-
.
-
├──
Cargo
.lock
-
├──
Cargo
.toml
-
├──
src
-
│ └──
lib
.rs
-
└──
tests
-
└──
sum
.rs
作为一个集成测试的例子,这是非常简单的。它的关键在于,当我们编写集成测试时,可以像程序库的任何其他用户那样使用被测试的软件包。
转载:https://blog.csdn.net/epubit17/article/details/113638396