302 lines
12 KiB
Markdown
302 lines
12 KiB
Markdown
#### 11测试
|
||
|
||
##### 编写测试
|
||
|
||
测试函数的三种操作:设置数据或状态,运行测试代码,断言结果。
|
||
|
||
在`fn`前加上`#[test]`,就是给这个函数带上了`test`属性。当执行`cargo test`命令时,Rust会调用标记了`test`属性的函数,并报告测试的结果。
|
||
|
||
`assert!`宏由标准库提供,它对一个布尔值进行判断,如为真则什么也不做,如为假则调用`panic!`宏。
|
||
|
||
`assert_eq!`和`assert_ne!`宏用于测试两个值是否相等,如断言失败则打印出这两个值的内容。
|
||
|
||
可以向`assert!`、`assert_eq!`、`assert_ne!`宏传递可选的信息参数,在它们的必需参数之后的参数都会传递给`format!`宏从而打印出来。
|
||
|
||
在`fn`前加上`#[should_panic]属性`,则函数panic的时候测试通过,函数没有panic的时候测试失败。这用来测试函数是否生成了panic。还可以给`should_panic`属性增加一个可选的`expected`参数,用以输出一些信息,在测试不通过时显示它从而方便定位问题。
|
||
|
||
也可以把`Result<T, E>`作为返回值编写测试,此时测试通过则返回`Ok(())`,测试失败则返回带有`String`的`Err`。
|
||
|
||
##### 控制测试
|
||
|
||
`cargo test`生成的二进制文件默认是并行运行的,所以测试不能相互依赖、或依赖任何共享的状态。
|
||
|
||
可以在`#[test]`后面增加`#[ignore]`来忽略某些测试。
|
||
|
||
详情可参考`man cargo-test`命令。
|
||
|
||
##### 测试的组织结构
|
||
|
||
单元测试存在于`src`目录下,与被测代码放在同一文件,位于用`#[cfg(test)]`标准的`tests`模块中。
|
||
|
||
`#[cfg(test)]`在执行`cargo test`才编译和运行测试代码,在执行`cargo build`时不这么做。因为单元测试的代码和源码在同一文件中,所以才需要`#[cfg(test)]`。
|
||
|
||
在单元测试里,是允许测试私有函数的。
|
||
|
||
集成测试存在于`tests`目录,cargo会把`tests`目录下的每一个文件当作一个单独的crate来编译。
|
||
|
||
集成测试把要测的库看成是外部的,所以需要用`use`来导入它。
|
||
|
||
`tests`目录对于cargo来说是一个特殊的目录,在执行`cargo test`时会自动编译这个目录下的文件。
|
||
|
||
`tests`目录中的子目录不会作为单独的crate编译,也不会作为测试结果的一部分出现在测试输出中。这对于集成测试中的子模块来说是有用的。使用子模块和使用其它模块没有区别,都是使用`mod`关键字。
|
||
|
||
#### 13 函数式编程
|
||
|
||
##### 闭包
|
||
|
||
数学上的闭包,个人理解,就是对某种运算封闭的一些对象的**最小**集合。在数据库里也有闭包的概念。Rust里的闭包是一个匿名函数,可以以变量或参数的形式存在,闭包可以捕获调用者作用域中的值。
|
||
|
||
使用函数获取结果,这个函数一定会在确定的位置被执行。而如果使用闭包,只有当需要这个结果的时候闭包才会执行。
|
||
|
||
闭包的定义以一对竖线开始,竖线之间是闭包的参数,如有多个参数则以逗号分隔;接下来则是闭包的语句块,如果是多行语句要使用大括号,如果只有一行则可省略大括号。
|
||
|
||
使用let语句定义一个变量为闭包,意味着这个变量是此闭包的定义,**而不是**此闭包的返回值。
|
||
|
||
调用闭包和调用函数的形式是一样的。
|
||
|
||
在定义闭包的时候,不必在参数和返回值上注明类型。因为,和函数不一样,闭包的接口不是暴露在外的,而是只关联于小范围的上下文中,编译器能可靠地推断参数和返回值的类型。注意,闭包的接口类型是**固定的**,如果调用两次闭包而给它传不同类型的参数,则编译器会报错。
|
||
|
||
为了提高计算效率,可以缓存闭包的返回值,这样在多次调用闭包的时候就可以避免重复计算。Rust的解决方案是:用一个结构体来存放闭包及其调用结果。为了在结构体里定义闭包,需要使用**泛型**和**特性绑定**。
|
||
|
||
`Fn`是由`std::ops`提供的特性。所有的闭包都要实现特性`Fn`、`FnMut`或`FnOnce`中的一个。
|
||
|
||
在结构体中使用闭包的形式:需要定义一个泛型,这泛型要用where语法特性绑定`Fn`(或者`FnMut`、`FnOnce`)。在结构体中需要定义一个此泛型的字段用以代表闭包的实例。在结构体中还需要定义一个`Option`类型的字段来保存闭包的结果。
|
||
|
||
在结构体中使用闭包的逻辑过程:在结构体里的闭包执行前,结构体里`Option`类型的字段值是`None`。第一次请求闭包,`Option`类型的字段值会存储闭包的结果。如果再次请求闭包的结果,则闭包不再执行,而是返回该`Option`字段的值。
|
||
|
||
在结构体里使用闭包,结构体里的字段应该是私有的,这样可以避免调用者不合理地改变字段的值。应创建`new`方法接收闭包并返回该结构体的实例。应该创建一个方法检查`Option`类型字段的值,如果是`None`则执行闭包并保存它的返回值,如果有值则直接返回该值。
|
||
|
||
使用值缓存的方法有两个小问题:1,一旦`Option`字段里有了值,则不管给闭包传入什么参数,返回值都不会改变。解决方法是用哈希映射来取代单独一个值。2,闭包传入的值和返回的值类型要一致。解决方法是引入范型。
|
||
|
||
闭包可以捕获它的调用者里的变量,而函数则不行。因为闭包和它的调用者在相同的作用域里。
|
||
|
||
闭包捕获其调用者环境的三种方式,对应三个特性:
|
||
|
||
1. `FnOnce` - 获取所有权。因为同一变量的所有权只能转移一次,所以叫Once。
|
||
2. `FnMut` - 获取可变的借用值。
|
||
3. `Fn` - 获取不可变的借用值。
|
||
|
||
`move`关键字可以强制闭包获取其调用者环境中值的所有权。
|
||
|
||
##### 迭代器
|
||
|
||
迭代器都实现了`Iterator`特性,其定义如下:
|
||
|
||
```rust
|
||
pub trait Iterator{
|
||
type Item;
|
||
fn next(&mut self) -> Option<Self::Item>;
|
||
// 方法的实现
|
||
}
|
||
```
|
||
|
||
`type Item`是说`Iterator`特性要求定义一个`Item`类型,作为`next`方法的返回值类型。`next`方法用于返回迭代器中的一个项。
|
||
|
||
`Iterator`特性中调用了`next`方法的方法被称为消费适配器。
|
||
|
||
`Iterator`特性中把当前迭代器变为其它类型迭代器的方法称为迭代器适配器。
|
||
|
||
只要实现了迭代器的`next`方法,就可以使用标准库中定义的其它`Iterator`特性的方法了,因为它们都使用了`next`方法的功能。
|
||
|
||
##### 循环VS迭代器
|
||
|
||
闭包和迭代器是Rust的**零成本抽象**之一。
|
||
|
||
#### 15 智能指针
|
||
|
||
**引用**是最简单的指针,**智能指针**是带有额外数据的引用。
|
||
|
||
智能指针与普通结构体的区别:`Deref`和`Drop`特性。`Deref`特性使智能指针表现得像引用一样。`Drop`特性定义了当智能指针离开作用域时运行的代码。
|
||
|
||
标准库中常用的智能指针有:
|
||
|
||
- `Box<T>`,用于在堆上分配值
|
||
- `Rc<T>`,一个引用计数类型,其数据可以有多个所有者
|
||
- `Ref<T>`和`RefMut<T>`,通过`RefCell<T>`访问。
|
||
|
||
**内部可变性**模式,把不可变类型暴露出来以改变其内部值。**引用循环**会泄漏内存。
|
||
|
||
##### Box\<T>
|
||
|
||
智能指针`Box<T>`把值放在堆上,把指向数据的指针留在栈上。
|
||
|
||
##### Deref特性
|
||
|
||
重载**解引用运算符**,把智能指针当作常规引用来对待。
|
||
|
||
##### Drop特性
|
||
|
||
`Drop`特性包含在prelude中,所以无需导入。
|
||
|
||
##### Rc\<T>
|
||
|
||
rc是引用计数的缩写,用于启动多所有权。
|
||
|
||
##### RefCell\<T>
|
||
|
||
**内部可变性**是Rust中的一个设计模式,它允许改变不可变引用的数据。
|
||
|
||
##### 引用循环
|
||
|
||
#### 16 无畏并发
|
||
|
||
**并发编程**是指程序的不同部分独立执行,**并行编程**是指程序的不同部分同时执行。
|
||
|
||
##### 使用线程实现并发
|
||
|
||
使用`tread::spawn`创建新线程。
|
||
|
||
使用`handle.join()`方法阻塞当前线程。
|
||
|
||
在闭包之前使用`move`,强制闭包获取其使用的值的所有权。
|
||
|
||
##### 使用消息传递实现并发
|
||
|
||
##### 使用共享状态实现并发
|
||
|
||
##### 使用Sync和Send特性实现并发
|
||
|
||
`Send`特性允许在线程间转移所有权。
|
||
|
||
`Sync`特性允许多线程访问。
|
||
|
||
#### 17 面向对象
|
||
|
||
##### 面向对象语言的特征
|
||
|
||
面向对象编程要求:对象中包含数据和行为。Rust语言可以使用`impl`块为结构体和枚举带上方法,从这个角度来看,Rust是面向对象的。
|
||
|
||
面向对象编程的思想:封装,即对象的实现细节不能被外部代码获取。Rust语言可以使用`pub`使模块、类型、函数和方法是公有的,默认情况下其它一切都是私有的。从这个角度来看,Rust是面向对象的。
|
||
|
||
面向对象编程的机制:继承,即对象之间可以相互继承。Rust语言使用的是**特性**而不是继承。从这个角度来看,Rust不是面向对象的。
|
||
|
||
##### trait对象
|
||
|
||
#### 18 模式
|
||
|
||
##### 模式的位置
|
||
|
||
`match`分支里包含了模式。
|
||
|
||
`if let`表达式里包含了模式。
|
||
|
||
`while let`表达式里包含了模式。
|
||
|
||
`for`循环里包含了模式。
|
||
|
||
`let`语句里包含了模式。
|
||
|
||
函数参数也可以是模式。
|
||
|
||
##### 模式的两种形式
|
||
|
||
模式的两种形式:可反驳的,不可反驳的。
|
||
|
||
不可反驳的:能匹配任何值的模式。函数参数、let语句和for循环只能接受此种模式。
|
||
|
||
可反驳的:匹配某些值的时候会失败的模式。if let、while let只能接受此种模式。
|
||
|
||
match匹配里前面的分支必须使用可反驳模式,最后一个则要支持不可反驳模式。
|
||
|
||
##### 模式的语法
|
||
|
||
匹配字面值。
|
||
|
||
匹配命名变量。
|
||
|
||
匹配多个模式。
|
||
|
||
匹配值的范围。
|
||
|
||
解构结构体、枚举、嵌套、元组。
|
||
|
||
忽略模式中的值。
|
||
|
||
匹配守护。
|
||
|
||
绑定。
|
||
|
||
#### 19 高级特征
|
||
|
||
##### 不安全性
|
||
|
||
使用`unsafe`关键字开启一个不安全的代码块。它将允许五类超级操作:
|
||
|
||
- 解引用裸指针
|
||
- 调用不安全的函数或方法
|
||
- 访问或修改可变静态变量
|
||
- 实现不安全特性
|
||
- 访问`union`的字段
|
||
|
||
##### 高级特性
|
||
|
||
关联类型。
|
||
|
||
为泛型指定一个默认的类型。
|
||
|
||
使用完全限定语法,以消除歧义。
|
||
|
||
超特性。
|
||
|
||
使用newtype模式在外部类型上实现外部特性。
|
||
|
||
##### 高级类型
|
||
|
||
使用newtype模式。
|
||
|
||
使用`type`关键字给现有类型另一个名字。
|
||
|
||
使用`!`在函数从不返回的时候充当返回值。
|
||
|
||
使用Sized特性来处理动态大小类型。
|
||
|
||
##### 高级函数和闭包
|
||
|
||
使用函数指针把函数传给函数。
|
||
|
||
使用trait对象来返回闭包。
|
||
|
||
##### 宏
|
||
|
||
- 宏包括了声明宏(*declarative* macros)和过程宏( *procedural* macros)。
|
||
|
||
声明宏用`macro_rules!`。
|
||
|
||
过程宏包含自定义宏`#[derive]`、类属性宏(Attribute-like macros)和类函数宏(Function-like macros)。
|
||
|
||
- 函数和宏的区别
|
||
|
||
函数必须声明参数个数与类型,而宏能接受不同数量的参数。
|
||
|
||
宏可以在预编译阶段展开,而函数是在编译阶段被编译的。
|
||
|
||
宏定义比函数定义更难阅读、理解和维护。
|
||
|
||
使用宏之前必须先有它的定义,而函数的定义可以出现在使用之后。
|
||
|
||
- 定义声明宏
|
||
|
||
```rust
|
||
#[macro_export]
|
||
macro_rules! vec {
|
||
( $( $x:expr ),* ) => {
|
||
{
|
||
let mut temp_vec = Vec::new();
|
||
$(
|
||
temp_vec.push($x);
|
||
)*
|
||
temp_vec
|
||
}
|
||
};
|
||
}
|
||
```
|
||
|
||
`#[macro_export]`使用宏可用。没有该属性宏不能被引入作用域。
|
||
|
||
`macro_rules! <name> {() => {};}`是宏定义的基本结构。
|
||
|
||
`=>`之前是要匹配的模式。$()用于捕获满足模式的值,模式`$x:expr`的意思是匹配任意的表达式,并给表达式取名为`$x`。随后的逗号表示在满足模式的代码之后可以出现逗号(也可以没有逗号),`*`的意思是可以匹配0次或多次。
|
||
|
||
`=>`之后是匹配到该模式后要执行的代码。
|
||
|