Rust 学习 10
archive time: 2022-06-11
这回来学一下生命周期
生命周期
Rust 中, 每个 引用 都有自己的生命周期, 是为了让引用保持有效的作用域
大多数情况下, 生命周期是可以被自动推导的, 不需要手动标注
但是有些情况下还是需要自己手动标注的, 例如 'static
pub fn derv<'a, F>(f: &'a F) -> Box<dyn Fn(f64) -> f64 + 'a>
where
F: Fn(f64) -> f64 + 'a,
{
let dx = 1e-6;
Box::new(move |x: f64| -> f64 { (f(x + dx) - f(x)) / dx })
}
在上面这个函数的例子里, 我们需要手动给泛型 Trait F
加上 'a
这样一个生命周期的约束
加上生命周期的约束是为了指明返回值和参数之间的关系
生命周期要解决的问题就是在 Cpp
中常见的错误 悬垂引用 (dangling reference)
let r;
{
let x = 6;
r = &x; // error here
}
println("{}", r);
上面这个例子, 由于 x
在离开作用域后就被 drop 掉了, 所以 r
就借用了一个无效的值,
这是错误的, 而 Rust 编译器可以检查出这种问题
Rust 编译器 使用了 Borrow Checker 通过比较作用域来判断所有的借用是否合法
example
这里看一个函数的例子
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
这里由于 x
和 y
这两个引用的生命周期有可能有区别, 所以需要显式标注生命周期
解决方法就是标注上生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
生命周期标注
生命周期以 '
开头, 而且是全小写, 标识符一般比较短, 常见命名就是 'a
生命周期一般放在引用符号之后
&i32 // ref
&'a i32 // explict lifetime ref
&'a mut i32 // mutable explict lifetime ref
不过单个生命周期标注没有意义, 标注一般用于说明多个引用之间的生命周期的 关系
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
这里表明 返回值 和 x
以及 y
的生命周期不短于 'a
, 即保证了返回的引用的有效性
从函数返回引用时, 返回值 的生命周期参数需要和其中一个参数的生命周期 匹配
结构体的生命周期
在 struct
里有 引用数据项 的时候, 需要在每个引用上添加生命周期标注
struct Test<'a> {
part: &'a str,
}
这里是为了保证在结构体实例有效的时候, 内部的引用也是有效的
生命周期规则
编译器有 3 条1规则用于推导引用的生命周期
- 每个引用类型的参数都有自己的生命周期
- 如果只有一个生命周期参数, 那么该生命周期就被赋给所有的输出生命周期参数
- 如果有多个输入生命周期参数, 但其中一个是
&self
或者&mut self
, 那么self
的生命周期会被赋给所有的输出生命周期参数
/// ex01
fn first_word(s: &str) -> &str
// => rule 1
fn first_word<'a>(s: &'a str) -> &str
// => rule 2
fn first_word<'a>(s: &'a str) -> &'a str
/// ex02
fn longest(x: &str, y: &str) -> &str
// => rule 1
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str
// 不能继续推导 报错
静态生命周期
'static
是比较特殊的生命周期, 表明在整个程序持续时间内都是有效的
所有的字符串字面值的生命周期都是 'static
一般没必要使用 'static
生命周期
测试
在写程序的时候, 为了验证程序的正确性, 往往需要进行一些测试
arrange, act, assert
用于测试的函数需要使用 #[test]
这样一个 attribute 来标注
attribute 就是一段 Rust 代码的元数据
如果写了一些测试函数要运行测试, 可以使用 cargo test
命令来执行测试
#[cfg(test)]
pub mod tests {
use crate::utils::numeric as N;
#[test]
fn do_test() {
let tol = 1e-6;
let cbrt = |x: f64| -> f64 { N::newton(&move |y: f64| -> f64 { x - y * y * y }, 1.0) };
assert!((cbrt(8.0) - 2.0).abs() < tol);
}
}
上面是一个比较经典的测试例子
自定义错误消息
assert!()
等宏是可以添加自定义信息的, 在测试失败的时候打印出来
#[test]
fn another() {
assert!(2 > 4, "\nhello {}\n", "ok");
}
/// ...
/*
---- tests::tests::another stdout ----
thread 'tests::tests::another' panicked at '
hello ok
', src\tests.rs:13:9
*/
上面就是一个自定义错误信息的例子, 我们测试失败了, 输出了 hello ok
should_panic
这里要介绍一个新的 特性(attribute), #[should_panic]
pub struct Guess {
val: u32
}
impl Guess {
pub fn new(val: u32) -> Self {
if val < 1 || val > 100 {
panic!("Guess value must between 1 and 100, got '{}'", val);
}
Guess { val }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
即, 对于会发生 恐慌(panic) 的函数或代码片段进行标注
如果真的发生了恐慌, 则测试成功
对于更加精确的测试, 可以使用 expected
参数来指明 panic!()
的报错包含的内容
/// ...
#[should_panic(expected = "anyway")]
/// ...
如果 panic 中不包含指定内容, 那么就算测试失败
测试里的 Result
之前有说过, Rust 里的一种更加常用的错误处理方式就是使用 Result<T, E>
相较于使用 assert!()
即 panic!()
, 还可直接返回 Result 类型来进行测试
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("2 + 2 != 4"))
}
}
cargo test
对于 cargo test
这个命令, 我们还可以使用参数来控制具体测试行为
测试线程数
如果要并行测试, 那么需要保证测试之间不会相互依赖
cargo test -- --test-threads=12
如果不想并行测试, 可以指定线程数为 1
, 即单线程测试即可
显示输出
默认情况下, 通过的测试不会输出相关测试的输出内容
如果想要输出所有的输出内容, 可以使用 --show-output
选项
cargo test -- --show-output
执行指定测试
有时候执行所有的测试是比较费时的, 而大多数情况是不需要进行所有测试的, 所以需要指定测试名称
cargo test <测试函数名>
但是这样只能测试一个函数, 即只能传递一个函数名
我们还可以使用
cargo test <测试名称>
来指定测试范围, 只要测试函数的名称中(包括模块名) 含有指定的测试名称, 那么就会被执行
忽略测试
对于一些比较麻烦的测试, 我们可以使用 #[ignore]
这个特性来忽略
如果要单独测试被忽略的测试, 可以使用 --ignored
参数
cargo test -- --ignored
测试组织
一般测试可以分为 单元测试 和 集成测试
单元测试 可以测试所有内容, 而 集成测试 一般只能测试 公开(pub) 内容
单元测试
单元测试一般使用 #[cfg(test)]
来标注
- 只有运行
cargo test
才会编译和运行 cargo build
不会编译
集成测试
集成测试完全位于被测试的内容之外
集成测试一般用于测试库的多个部分是否能正常地工作
集成测试一般位于 <projRoot>/tests
目录下, 与 src
目录平级
C:.
| .gitignore
| Cargo.lock
| Cargo.toml
|
+---src
| | lib.rs
| | main.rs
| |
| \---utils
| mod.rs
| numeric.rs
|
\---tests
int_tests.rs
如果要测试单独地某个集成测试的文件, 可以使用
cargo test --test <文件名>
对于集成测试子模块, 如一些帮助函数, 可以使用文件夹分隔
即,tests/
下的子目录不会被视为单独的 crate 来编译, 不会被测试
但是注意, 集成测试只针对于 库类型 的包, 对于二进制包, 由于无法对外暴露函数, 故无法使用集成测试, 只能使用 单元测试
所以一般都是二进制包和库包混合的形式, 方便进行测试
好, 今天就学到这里罢
到目前为止, 未来会添加更多