rust chap20 标准库的其他功能

 

标准库还有线程、通道、IO、文件系统能很多功能

标准库还有线程、通道、IO、文件系统能很多功能

线程

spawn 函数提供了创建本地操作系统(native OS)线程的机制

创建 NTHREADS = 10 个线程

  • thread::spawn(move || { ... })

等待线程结束

  • let _ = child.join()

创建的线程由操作系统调度

use std::thread;
static NTHREADS: i32 = 10;

fn main() {
    let mut children = vec![];
    
    for i in 0..NTHREADS {
        children.push(thread::spawn(
        	move || println!("线程[{}]", i)))
    }
    for child in children {
        let _ = child.join();
    }
}

通道

channel:线程之间的通信的异步通道

两个端点之间信息的流动是单工的,甚至都不是半双工的

  • Sender
  • Receiver

多发单收示例

  • 创建发送端 Sender 和接收端 Receiver

    let (tx, rx): (Sender<i32>, Receiver<i32>) = mpsc::channel();
    
  • 创建10个发送器线程

    for id in 0..10 {
        let thread_tx = tx.clone()
          
        thread::spawn(move || {
            thread_tx.send(id).unwrap();
        });
    }
    
    • 发送端可以复制
      • let thread_tx = tx.clone()
  • 用一个容器接受各发送端线程发送来的消息

    let mut ids = Vec::with_capacity(10);
    for _ in 0..10 {
        ids.push(rx.recv());
    }
    println!("{:?}", ids);
    
  • 最终结果

    线程[0]发送数据...
    线程[1]发送数据...
    ... ...
    线程[5]发送数据...
    线程[6]发送数据...
    [Ok(0), Ok(1), Ok(2), Ok(3), Ok(4), Ok(7), Ok(8), Ok(9), Ok(5), Ok(6)]
    

路径

文件系统的文件路径由Paht结构体表示

  • posix::Path,针对类 UNIX 系统
  • windows::Path,针对 Windows
  • Path 可从 OsStr 类型创建
  • Path 在内部不是 UTF-8 字符串,存储为若干字节(Vec<u8>)的 vector
  • Path 转化成 &str 并非零开销
  • 且可能失败(因此它 返回一个 Option

文件输入输出(I/O)

File 结构体表示一个被打开的文件

  • 包裹了一个文件描述符
  • 对所表示的文件有读写能力
  • File 所有方法返回 io::Result<T> = Result<T, io::Error> 类型
    • 因为在进行文件 I/O 操作时可能出现各种错误

打开文件 open

File::open(&Path::new("readme.txt")) 只读方式打开文件

File 可以自动调用 drop 方法,释放打开的文件

file.read_to_string(&mut content) 需要 std::io::prelude::* 中的特性 read_to_string

创建文件 create

  • 设置文件路径

    let filepath = Path::new("output/readme.txt");
    
  • 创建文件

    let mut file = match File::create(&filepath) {
        Err(why) => panic!("创建文件[{:?}]失败:[{}]", filepath, why),
        Ok(file) => file,
    }
    
  • 写入内容

    let content = "写入内容:let write\n";
    match file.write_all(content.as_bytes()) {
        Err(why) => println!("写入文件[{:?}]失败:[{}]", filepath, why),
        Ok(_) => println!("写入完成"),
    }
    

读取行

打开文件,并返回行迭代器

fn read_lines<P>(filepath: P) -> io::Result<io::Lines<io::BufReader<File>>>
    where P: AsRef<Path> {
        file = File::open(filepath)?;
        Ok(io::BufReader::new(file).lines())
}

读取并行迭代器的内容

if let Ok(lines) = read_lines("./hosts.txt") {
    for line in lines {
        if let Ok(ip) = line {
            println!("{}", ip);
        }
    }
}

子进程

创建子进程:process::Command 结构体

子进程输出:process::Output 结构体

通过子进程获取 rustc 的版本号

  • 执行 rustc --version

    let version = Command::new("rustc")
    				.arg("--version")
    				.output()
    				.unwrap_or_else(|e| {
                        panic!("执行失败:{}", e)
    });
    
  • 将执行结果转换成 String 类型

    if version.status.success() {
        let s = String::from_utf8_loosy(&version.stdout);
    } else {
        let s = String::from_utf8_loosy(&version.stderr);
    }
    

管道

std::Child 结构体代表了一个正在运行的子进程

它暴露了 stdinstdoutstderr句柄

可以通过管道与所代表的进程交互

启动 wc 进程,暴露 stdinstdout

let process = match Command::new("wc")
	.stdin(Stdio::piped())
	.stdout(Stdio::piped())
	.spawn() {
        Err(e) => panic!("couldn't spawn wc: {}", e),
        Ok(process) => process,
};

将字符串写入 wcstdin

let content = "我输入wc的内容\n";
match process.stdin.unwrap().write_all(content.as_bytes()) {
    Err(e) => panic!("输入失败:{}", e),
    Ok(_) => println!("内容写入wc"),
}

wc 打印出写入的内容:

let mut s = String::new();
match process.stdout.unwrap().read_to_string(&mut s) {
    Err(why) => panic!("couldn't read wc stdout: {}", why),
    Ok(_) => print!("wc responded with:\n{}", s),
}

等待

调用 Child::wait

  • 等待 process::Child 完成
  • 返回 process::ExitStatus

文件系统操作

  • 以只读形式打开文件
    • File::open(path)
  • 读取文件内容到字符串
    • file.read_to_string(&mut s)
  • 创建新文件
    • File::create(path)
  • 写内容到文件中
    • file.write_all(s.as_bytes())
  • 以指定的行为打开文集
    • 如果文件存在,在末尾追加内容
      • OpenOptions::new().append(true).open("server.log")?
    • 如果文件存在则失败
      • OpenOptions::new().write(true).create_new(true).open("file.txt")?

创建目录

  • fs::create_dir("a")?

递归创建目录

fs::create_dir_all("a/c/d")?

touch 一个文件

  • touch(&Path::new("a/c/e.txt").unwrap())

创建符号链接

  • unix::fs::symlink("../b.txt", "a/c/d.txt")

读取目录的内容

  • fs::read_dir("a")

删除文件

  • fs::move_file("a/c/e.xtx")

删除空目录

  • fs::remove_dir("a/c/d")

程序参数

标准库

  • 命令行参数解析:std::env::args
    • 返回迭代器
    • 对每个参数举出一个字符串
  • 参数解析用 clap

参数解析

模式匹配来解析简单的参数

外部语言函数接口

Rust 提供了到 C 语言库的外部语言函数接口(Foreign Function Interface,FFI)

外部语言函数必须在一个 extern 代码块中声明

且该代码块要带有一个包含库名称 的 #[link] 属性

链接外部 c 语言的 libm

#[link(name = "m")]
extern {
    fn csqrtf(z: Complex) -> Complex;
    // 计算单精度复数的复变余弦
    fn ccosf(z: Complex) -> Complex;
}

定义给外语言函数使用的结构体

#[repr(C)]
#[derive(Clone, Copy)]
struct Complex {
    re: f32,
    im: f32
}

impl fmt::Debug for Complex {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.im < 0. {
            write!(f, "{}-{}i", self.re, -self.im)
        } else {
            write!(f, "{}+{}i", self.re, self.im)
        }
    }

使用外语言函数是不安全的,需要注明:

let z = Complex { re: -1., im: 0. };
let z_sqrt = unsafe { csqrtf(z) };

安全封装外语言函数后再使用

fn cos(z: Complex) -> Complex {
    unsafe { ccosf(z) }
}
prilntln!("cos({:?}) = {:?}", z, cos(z));

测试

单元测试

  • 单元测试一次仅能单独测试一个模块

  • 测试函数的函数体通常会进行一些配置

  • 运行我们想要测试的代码,然后 断定(assert)结果是我们所期望的

  • 测试函数放到 tests 的模块中

    • 模块设置#[cfg(test)] 属性
    • 模块中测试函数设置#[test] 属性
  • 测试的辅助宏

    • assert!(exp, print_str):如果 exp==false,引发 panic
    • assert_eq!(left, right)assert_ne!(left, right) - 检验左右两边是否 相等/不等
  • 示例

    • 创建 test 模块,并且设置 test 模块属性

      #[cfg(test)]
      mod tests {
          ...
      }
      
    • 创建测试单元,并设置 test 函数属性

      #[test]
      fn test_add() {
          assert_eq!(add(1, 2), 3);
      }
          
      #[test]
      fn test_bad_add() {
          assert_eq!(bad_add(1, 2), 3);
      }
      
    • 运行结果

      test tests::test_bad_add ... FAILED
      test tests::test_add ... ok
      

    测试 panic

    测试函数是不是在特定条件下引发了希望的 panic

    • 使用 #[should_panic] 属性
    • 这个属性可以接受可选参数 expected = 以指定 panic 时输出的消息
    #[test]
    #[should_panic]
    fn test_any_panic() {
        always_panic(1, 0);
    }
      
    #[test]
    #[should_panic(expected = "Divide result is zero")]
    fn test_specific_panic() {
        divide_non_zero_result(1, 10);
    }
      
    // divide_non_zero_result: `panic!("Divide result is zero");`
    
    • 结果
    test tests::test_divide ... ok
    test tests::test_specific_panic ... ok
    

    运行特定的测试

    把测试名称传给 cargo test 命令

    cargo test test_any_panic

    运行多个测试,可以仅指定测试名称中的一部分

    • cargo test panic

      test tests::test_any_panic ... ok
      test tests::test_specific_panic ... ok
      

    忽略测试

    • 把属性 #[ignore] 赋予测试以排除某些测试

      #[test]
      #[ignore]
      fn ignored_test() {
          ...
      }
      
      • 结果

        test tests::ignored_test ... ignored
        
    • 使用 cargo test -- --ignored 命令来运行它们

      $ cargo test -- --ignored
      test tests::ignored_test ... ok
      

文档测试

  • 文档注释使用 markdown 语法
  • 支持代码块
  • 注释中的代码块也会被编译并且用作测试
/// 下面的函数将两数相除
///
/// # Example
///
/// ```
/// let result = doccomments::div(10, 2);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
///
/// 如果第二个参数是 0,函数将会 panic。
///
/// ```rust,should_panic
/// // panics on division by zero
/// doccomments::div(10, 0);
/// ```
  • 文档中可以加入 #Examle# Panics 等部分
  • 文档中还可以加入 rust, should_panic 的属性

文档测试的目的

作为使用函数功能的使用例子,这是最重要的指导原则之一

为了在文档中使用 ? 操作符(因 main() -> ()),需要编写掩藏源码

被隐藏的行以 #开始,但它们仍然会被编译!

/// 在文档测试中使用隐藏的 `try_main`。
///
/// ```
/// # // 被隐藏的行以 `#` 开始,但它们仍然会被编译!
/// # fn try_main() -> Result<(), String> { // 隐藏行包围了文档中显示的函数体
/// let res = try::try_div(10, 2)?;
/// # Ok(()) // 从 try_main 返回
/// # }
///
/// # fn main() { // 开始主函数,其中将展开 `try_main` 函数
/// #    try_main().unwrap(); // 调用并展开 try_main,这样出错时测试会 panic
/// # }

文档中显示的只有 let res = try::try_div(10, 2)?;

集成测试

  • 集成测试是 crate 外部的测试

  • 且仅使用 crate 的公共接口

  • 集成测试的目的是检验库的各部分是否能够正确地协同工作

  • cargo 在与 src 同级别的 tests 目录寻找集成测试

  • 测试流程

    • 声明外部 crate

      extern crate adder;
      
    • 测试外部 crate 中的函数

      #[test]
      fn test_add() {
          assert_eq!(adder::add(3, 2), 5);
      }
      
    • 使用 cargo test 命令

      $ cargo test
      
  • tests 目录中的每一个 Rust 源文件都被编译成一个单独的 crate

    • 要共享代码

      • 创建具有公用函数的模块 tests/common.rstest/common/mod.rs

        pub fn setup() {
            // 一些配置代码,比如创建文件/目录,开启服务器等等。
        }
        
      • 在测试中导入并使用它

        // 被测试的外部 crate。
        extern crate adder;
              
        // 导入共用模块。
        mod common;
              
        #[test]
        fn test_add() {
            // 使用共用模块。
            common::setup();
            assert_eq!(adder::add(3, 2), 5);
        }
        

开发依赖

  • 依赖要写在 Cargo.toml[dev-dependencies] 部分

  • 这些依赖不会传播给其他依赖于这个包的 crate

  • 示例:使用 pretty_assertions 依赖,它扩展了标准的 assert!

    • Cargo.toml 中添加

      [dev-dependencies]
      pretty_assertions = "0.4.0"
      
    • 在代码中声明外部 crate

      #[cfg(test)]
      #[macro_use]
      extern crate pretty_assertions;
      
    • 使用

      #[cfg(test)]
      mod tests {
          use super::*;  // 使用pretty_assertions
          
          #[test]
          fn test_add() {
              assert_eq!(add(2, 3), 5);
          }
      }
      

不安全操作

不安全代码块用于避开编译器的保护策略

  • 解引用裸指针
  • 通过 FFI 调用函数
  • 调用不安全的函数
  • 内联汇编

原始指针

原始指针和引用(&T)有类似的功能

  • 但引用总是安全的

  • 因为借用检查器保证了它指向一个有效的数据

  • 解引用一个裸指针只能通过 unsafe 代码块执行

    unsafe { assert!(*raw_p == 10); }
    

调用不安全函数

函数声明为 unsafe

  • 在使用它时保证正确性不再是编译器
  • 而是程序员
  • 否则程序的行为是未定义的(undefined)
  • 不能预测会发生什么

兼容性

原始标志符

原始标识符允许使用不允许的关键字

使用 2015 版 Rust 编译的 crate foo,它导出一个名为 try 的函数

try 在 2018 版 中是关键字

使用 foo::try() 编译失败,编译器指出 try 是关键字而不是标识符

可以 foo:r#try() 重新使用

补充

运行 Rustdoc,文档注释就会编译成文档