12 KiB
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,闭包传入的值和返回的值类型要一致。解决方法是引入范型。
闭包可以捕获它的调用者里的变量,而函数则不行。因为闭包和它的调用者在相同的作用域里。
闭包捕获其调用者环境的三种方式,对应三个特性:
FnOnce
- 获取所有权。因为同一变量的所有权只能转移一次,所以叫Once。FnMut
- 获取可变的借用值。Fn
- 获取不可变的借用值。
move
关键字可以强制闭包获取其调用者环境中值的所有权。
迭代器
迭代器都实现了Iterator
特性,其定义如下:
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)。 -
函数和宏的区别
函数必须声明参数个数与类型,而宏能接受不同数量的参数。
宏可以在预编译阶段展开,而函数是在编译阶段被编译的。
宏定义比函数定义更难阅读、理解和维护。
使用宏之前必须先有它的定义,而函数的定义可以出现在使用之后。
-
定义声明宏
#[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次或多次。=>
之后是匹配到该模式后要执行的代码。