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
    }
}

这里由于 xy 这两个引用的生命周期有可能有区别, 所以需要显式标注生命周期

解决方法就是标注上生命周期

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规则用于推导引用的生命周期

  1. 每个引用类型的参数都有自己的生命周期
  2. 如果只有一个生命周期参数, 那么该生命周期就被赋给所有的输出生命周期参数
  3. 如果有多个输入生命周期参数, 但其中一个是 &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 来编译, 不会被测试

但是注意, 集成测试只针对于 库类型 的包, 对于二进制包, 由于无法对外暴露函数, 故无法使用集成测试, 只能使用 单元测试

所以一般都是二进制包和库包混合的形式, 方便进行测试


好, 今天就学到这里罢

1

到目前为止, 未来会添加更多