标准库还有线程、通道、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
,针对 WindowsPath
可从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
结构体代表了一个正在运行的子进程
它暴露了 stdin
,stdout
和 stderr
句柄
可以通过管道与所代表的进程交互
启动 wc
进程,暴露 stdin
和 stdout
let process = match Command::new("wc")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn() {
Err(e) => panic!("couldn't spawn wc: {}", e),
Ok(process) => process,
};
将字符串写入 wc
的 stdin
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.rs
或test/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,文档注释就会编译成文档