Doolittle:你有什么具体证据能证明你的存在?炸弹-20:嗯……好吧……我思故我在。 Doolittle:很好。非常好。但你又如何知道其他事物的存在呢?炸弹-20:我的感官感受到了。 ——科幻喜剧《暗星》(Dark Star)
Rust 标准库中的输入和输出的特性是围绕 3 个特型组织的,即 Read、BufRead 和 Write。 实现了 Read 的值具有面向字节的输入方法。它们叫作读取器。实现了 BufRead 的值是缓冲读取器。它们支持 Read 的所有方法,外加读取文本行等方法。 实现了 Write 的值能支持面向字节和 UTF-8 文本的输出。它们叫作写入器。 图 18-1 展示了这 3 个特型以及几个读取器类型和写入器类型的示例。

图 18-1:Rust 的 3 个主要 I/O 特型和一些实现了它们的类型 在本章中,我们会讲解如何使用这些特型及其方法,涵盖了图 18-1 中所示的这些读取器类型和写入器类型,并展示了与文件、终端和网络进行交互的其他方式。
18.1 读取器与写入器
你的程序可以从读取器中读取一些字节。例如:
使用 std::fs::File::open(filename) 打开的文件; std::net::TcpStream,用于通过网络接收数据; std::io::stdin(),用于从进程的标准输入流中进行读取;
std::io::Cursor<&[u8]> 值和 std::io::Cursor
use std::io::{self, ErrorKind, Read, Write};
const DEFAULT_BUF_SIZE: usize = 8 * 1024;
pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> io::Result<u64>
where
R: Read,
W: Write,
{
let mut buf = [0; DEFAULT_BUF_SIZE];
let mut written = 0;
loop {
let len = match reader.read(&mut buf) {
Ok(0) => return Ok(written),
Ok(len) => len,
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
};
writer.write_all(&buf[..len])?;
written += len as u64;
}
}
这是 Rust 标准库中 std::io::copy() 的实现。由于是泛型的,因此可以使用它将数据从 File 复制到 TcpStream,从标准输入复制到内存中的 Vec
use std::io::prelude::*;
你会在本章中看到一两次这种写法。我们还习惯于导入 std::io 模块本身:
use std::io::{self, ErrorKind, Read, Write};
此处的 self 关键字将 io 声明成了 std::io 模块的别名。这样, std::io::Result 和 std::io::Error 就可以更简洁地写为 io::Result 和 io::Error 了。
18.1.1 读取器
std::io::Read 有以下几个读取数据的方法。所有这些方法都需要对读取器本身进行可变引用。
reader.read(&mut buffer)(读取)
从数据源中读取一些字节并将它们存储在给定的 buffer 中。
buffer 参数的类型是 &mut[u8]。此方法最多会读取 buffer.len() 字节。
返回类型是 io::Result
18.1.2 缓冲读取器
为了提高效率,可以对读取器和写入器进行缓冲,这基本上意味着它们有一块内存(缓冲区),用于保存一些输入数据或输出数据。这可以减少一些系统调用,如图 18-2 所示。应用程序会从 BufReader 中读取数据,在本例中是通过调用其 .read_line() 方法实现的。 BufReader 会依次从操作系统获取更大块的输入。

图 18-2:缓冲文件读取器
图 18-2 未按比例绘制。BufReader 缓冲区的实际大小默认为几千字节,因此一次 read 系统调用就可以服务数百次 .read_line() 调用。这很重要,因为系统调用很慢。
(如图 18-2 所示,操作系统也有自己的缓冲区。同理:系统调用固然慢,但从磁盘读取数据更慢。)
缓冲读取器同时实现了 Read 特型和 BufRead 特型,后者添加了以下方法。
reader.read_line(&mut line)(读一行)
读取一行文本并将其追加到 line,line 是一个 String。行尾的换行符 ‘\n’ 包含在 line 中。
如果输入带有 Windows 风格的行尾结束符号 “\r\n”,则这两个字符都会包含在 line 中。
返回值是 io::Result
18.1.3 读取行
下面是一个用于实现 Unix grep 实用程序的函数,该函数会在多行文本(通常是通过管道从另一条命令输入的文本)中搜索给定字符串:
use std::io;
use std::io::prelude::*;
fn grep(target: &str) -> io::Result<()> {
let stdin = io::stdin();
for line_result in stdin.lock().lines() {
let line = line_result?;
if line.contains(target) {
println!("{}", line);
}
}
Ok(())
}
因为要调用 .lines(),所以需要一个实现了 BufRead 的输入源。在这种情况下,可以调用 io::stdin() 来获取通过管道传输给我们的数据。但是,Rust 标准库使用互斥锁保护着 stdin。因此要调用 .lock() 来锁定 stdin 以供当前线程独占使用,这会返回一个实现了 BufRead 的 StdinLock 值。在循环结束时,StdinLock 会被丢弃,释放互斥锁。(如果没有互斥锁,那么当两个线程试图同时从 stdin 读取时就会导致未定义行为。C 语言也有相同的问题,且以相同的方式来解决:所有 C 标准输入函数和输出函数都会在幕后获得锁。唯一的不同在于,在 Rust 中,锁是 API 的一部分。) 该函数的其余部分都很简单:它会调用 .lines() 并对生成的迭代器进行循环。因为这个迭代器会生成 Result 值,所以要用 ? 运算符检查错误。 假如我们想完善这个 grep 程序,让它支持在磁盘上搜索文件。可以把这个函数变成泛型函数:
fn grep<R>(target: &str, reader: R) -> io::Result<()>
where
R: BufRead,
{
for line_result in reader.lines() {
let line = line_result?;
if line.contains(target) {
println!("{}", line);
}
}
Ok(())
}
现在可以将 StdinLock 或带缓冲的 File 传给它:
let stdin = io::stdin();
grep(&target, stdin.lock())?; // 正确
let f = File::open(file)?;
grep(&target, BufReader::new(f))?; // 同样正确
请注意,File 不会自动缓冲。File 实现了 Read 但没实现 BufRead。但是,为 File 或任意无缓冲读取器创建缓冲读取器很容易。BufReader::new(reader) 就是做这个的。(要设置缓冲区的大小,请使用 BufReader::with_capacity(size, reader)。) 在大多数语言中,文件默认是带缓冲的。如果想要无缓冲的输入或输 出,就必须弄清楚如何关闭缓冲。在 Rust 中,File 和 BufReader 是两个独立的库特性,因为有时你想要不带缓冲的文件,有时你想要不带文件的缓冲(例如,你可能想要缓冲来自网络的输入)。下面是一个包括错误处理和一些粗略的参数解析的完整程序。
// grep——搜索stdin或其他文件,以便用给定的字符串进行逐行匹配
use std::error::Error; use std::io::{self, BufReader}; use std::io::prelude::*; use std::fs::File; use std::path::PathBuf;
fn grep<R>(target: &str, reader: R) -> io::Result<()> where R: BufRead
{
for line_result in reader.lines() { let line = line_result?; if line.contains(target) { println!("{}", line);
}
}
Ok(())
}
fn grep_main() -> Result<(), Box<dyn Error>> {
// 获取命令行参数。第一个参数是要搜索的字符串,其他参数是一些文件名 let mut args = std::env::args().skip(1); let target = match args.next() {
Some(s) => s,
None => Err("usage: grep PATTERN FILE...")?
};
let files: Vec<PathBuf> = args.map(PathBuf::from).collect();
if files.is_empty() { let stdin = io::stdin(); grep(&target, stdin.lock())?;
} else {
for file in files { let f = File::open(file)?;
grep(&target, BufReader::new(f))?;
}
}
Ok(())
}
fn main() { let result = grep_main(); if let Err(err) = result { eprintln!("{}", err); std::process::exit(1);
}
}
18.1.4 收集行
有些读取器方法(包括 .lines())会返回生成 Result 值的迭代 器。当你第一次想要将文件的所有行都收集到一个大型向量中时,就会遇到如何摆脱 Result 的问题:
// 正确,但不是你想要的
let results: Vec<io::Result<String>> = reader.lines().collect();
// 错误:不能把Result的集合转换成Vec<String>
let lines: Vec<String> = reader.lines().collect();
第二次尝试无法编译:遇到这些错误怎么办?最直观的解决方法是编写一个 for 循环并检查每个条目是否有错:
let mut lines = vec![];
for line_result in reader.lines() {
lines.push(line_result?);
}
这固然没错,但这里最好还是用 .collect(),事实上确实可以做到。只要知道该请求哪种类型就可以了:
let lines = reader.lines().collect::<io::Result<Vec<String>>>()?;
这是怎么做到的呢?标准库中包含了 Result 对 FromIterator 的实现(在线文档中这很容易被忽略),这个实现让一切成为可能:
impl<T, E, C> FromIterator<Result<T, E>> for Result<C, E> where C: FromIterator<T> {}
这需要仔细阅读,但确实是一个很好的技巧。假设 C 是任意集合类型,比如 Vec 或 HashSet。只要已经知道如何从 T 值的迭代器构建出 C,就可以从生成 Result
18.1.5 写入器如前所述,输入主要是用方法完成的,而输出略有不同。
本书一直在使用 println!() 生成纯文本输出:
println!("Hello, world!");
println!("The greatest common divisor of {:?} is {}", numbers, d);
println!(); // 打印空行
还有不会在行尾添加换行符的 print!() 宏,以及会写入标准错误流的 eprintln! 宏和 eprint! 宏。所有这些宏的格式化代码都和 format! 宏一样,17.4 节曾讲解过。 要将输出发送到写入器,请使用 write!() 宏和 writeln!() 宏。它们和 print!() 和 println!() 类似,但有两点区别:
writeln!(io::stderr(), "error: world not helloable")?;
writeln!(
&mut byte_vec,
"The greatest common divisor of {:?} is
{}",
numbers, d
)?;
一是每个 write 宏都接受一个额外的写入器作为第一参数。二是它们会返回 Result,因此必须处理错误。这就是为什么要在每行末尾使用 ? 运算符。
print 宏不会返回 Result,如果写入失败,它们只会 panic。由于写入的是终端,所以极少失败。
Write 特型有以下几个方法。
writer.write(&buf)(写入)
将切片 buf 中的一些字节写入底层流。此方法会返回 io::Result
let file = File::create("tmp.txt")?;
let writer = BufWriter::new(file);
要设置缓冲区的大小,请使用 BufWriter::with_capacity(size, writer)。 当丢弃 BufWriter 时,所有剩余的缓冲数据都将写入底层写入器。但是,如果在此写入过程中发生错误,则错误会被忽略。(由于错误 发生在 BufWriter 的 .drop() 方法内部,因此没有合适的地方来报告。)为了确保应用程序会注意到所有输出错误,请在丢弃带缓冲的写入器之前将它手动 .flush() 一下。
18.1.6 文件
下面是本书已经介绍过的打开文件的两个方法。
File::open(filename)(打开)
打开现有文件进行读取。此方法会返回一个
io::Result
use std::fs::OpenOptions;
let log = OpenOptions::new()
.append(true) // 如果文件已存在,则追加到末尾
.open("server.log")?;
let file = OpenOptions::new()
.write(true)
.create_new(true) // 如果文件已存在,则失败
.open("new_file.txt")?;
方法 .append()、.write()、.create_new() 等是可以链式调用的:每个方法都会返回 self。这种链式调用的设计模式很常见,所以在 Rust 中它有一个专门的名字——构建器(builder)。另一个例子是 std::process::Command。有关 OpenOptions 的详细信息,请参阅在线文档。 File 打开后的行为就和任何读取器或写入器一样。如果需要,可以为它添加缓冲区。File 在被丢弃时会自动关闭。
18.1.7 寻址
File 还实现了 Seek 特型,这意味着你可以在 File 中“跳来跳去”,而不是从头到尾一次性读取或写入。Seek 的定义如下:
pub trait Seek {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64>;
}
pub enum SeekFrom {
Start(u64),
End(i64),
Current(i64),
}
此枚举让 seek 方法表现得很好:可以用 file.seek(SeekFrom::Start(0)) 倒回到开头,还能用 file.seek(SeekFrom::Current(-8)) 回退几字节,等等。 在文件中寻址很慢。无论使用的是硬盘还是固态驱动器(SSD),每一次寻址的开销都接近于读取数兆字节的数据。 18.1.8 其他读取器与写入器类型迄今为止,本章一直在使用 File 作为示范的主力,但还有许多其他有用的读取器类型和写入器类型。 io::stdin()(标准输入) 返回标准输入流的读取器。类型是 io::Stdin。由于它被所有线程共享,因此每次读取都会获取和释放互斥锁。 Stdin 有一个 .lock() 方法,该方法会获取互斥锁并返回 io::StdinLock,这是一个带缓冲的读取器,在被丢弃之前会持有互斥锁。因此,对 StdinLock 的单个操作就避免了互斥开销。18.1.3 节展示过使用此方法的示例代码。 由于技术原因,不能直接调用 io::stdin().lock()1。这个锁 持有对 Stdin 值的引用,这意味着此 Stdin 值必须存储在某个能让它“活得”足够长的地方: 1从 Rust 1.8 开始,已经可以直接调用 io::stdin().lock() 了。——译者注
let stdin = io::stdin();
let lines = stdin.lock().lines(); // 正确
io::stdout()(标准输出)和 io::stderr()(标准错误)
返回标准输出流(Stdout)类型和标准错误流(Stderr)类型的写入器。它们也有互斥锁和 .lock() 方法。
Vec
use std::process::{Command, Stdio};
let mut child = Command::new("grep")
.arg("-e")
.arg("a.*e.*i.*o.*u")
.stdin(Stdio::piped())
.spawn()?;
let mut to_child = child.stdin.take().unwrap();
for word in my_words {
writeln!(to_child, "{}", word)?;
}
drop(to_child); // 关闭grep的stdin,以便让它退出 child.wait()?;
child.stdin 的类型是 Optionstd::process::ChildStdin,这里在建立子进程时使用了 .stdin(Stdio::piped()),因此当 .spawn() 成功时, child.stdin 必然已经就位。如果没提供,那么 child.stdin 就是 None。 Command 还有两个类似的方法 .stdout() 和 .stderr(),可用于请求 child.stdout 和 child.stderr 中的读取器。 std::io 模块还提供了一些返回普通读取器和写入器的函数。 io::sink()(地漏) 这是无操作写入器。所有的写入方法都会返回 Ok,但只是把数据扔掉了。 io::empty()(空白) 这是无操作读取器。读取总会成功,但只会返回“输入结束” (EOF)。 io::repeat(byte)(重复) 返回一个会无限重复给定字节的读取器。
18.1.9 二进制数据、压缩和序列化
许多开源 crate 建立在 std::io 框架之上,以提供额外的特性。 byteorder crate 提供了 ReadBytesExt 特型和 WriteBytesExt 特型,为所有读取器和写入器添加了二进制输入和输出的方法:
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
let n = reader.read_u32::<LittleEndian>()?;
writer.write_i64::<LittleEndian>(n as i64)?;
flate2 crate 提供了用于读取和写入 gzip 数据的适配器方法:
use flate2::read::GzDecoder;
let file = File::open("access.log.gz")?;
let mut gzip_reader = GzDecoder::new(file);
serde crate 及其关联的格式类 crate(如 serde_json)实现了序列化和反序列化:它们在 Rust 结构体和字节之间来回转换。 11.2.2 节曾简单提到过这个 crate。现在可以仔细看看了。 假设我们有一些数据(文字冒险游戏中的映射表)存储在 HashMap 中:
type RoomId = String; // 每个房间都有唯一的名字 type RoomExits = Vec<(char, RoomId)>; // ……并且存在一个出口列表 type RoomMap = HashMap<RoomId, RoomExits>; // 房间名和出口的简单映射表
// 创建一个简单映射表
let mut map = RoomMap::new();
map.insert(
"Cobble Crawl".to_string(),
vec![('W', "Debris Room".to_string())],
);
map.insert(
"Debris Room".to_string(),
vec![
('E', "Cobble Crawl".to_string()),
('W', "Sloping Canyon".to_string()),
],
);
将此数据转换为 JSON 输出只需一行代码:
serde_json::to_writer(&mut std::io::stdout(), &map)?;
在内部,serde_json::to_writer 使用了 serde::Serialize
特型的 serialize 方法。该库会将 serde::Serialize 特型附加到所有它知道如何序列化的类型中,包括我们的数据中出现过的类型:字符串、字符、元组、向量和 HashMap。
serde 很灵活。在这个程序中,输出是 JSON 数据,因为我们选择了
serde_json 序列化器。其他格式(如 MessagePack)也有对应的序列化器支持。同样,可以将此输出发送到文件、Vec
{"Debris Room":[["E","Cobble Crawl"],["W","Sloping Canyon"]],"Cobble Crawl": [["W","Debris Room"]]}
serde 还包括对供派生的两个关键 serde 特型的支持:
#[derive(Serialize, Deserialize)]
struct Player {
location: String,
items: Vec<String>,
health: u32,
}
这里的 #[derive] 属性会让编译多花费一点儿时间,所以当你在 Cargo.toml 文件中将它列为依赖项时,要明确要求 serde 支持它。下面是要用在上述代码中的内容:
[dependencies]
serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"
有关详细信息,请参阅 serde 文档。简而言之,构建系统会为 Player 自动生成 serde::Serialize 和 serde::Deserialize 的实现,因此序列化 Player 的值很简 单:
serde_json::to_writer(&mut std::io::stdout(), &player)?;
输出如下所示。
{"location":"Cobble Crawl","items":["a wand"],"health":3}
18.2 文件与目录
前面已经展示了如何使用读取器和写入器,接下来的几节将介绍 Rust 用于处理文件和目录的特性,这些特性位于 std::path 模块和 std::fs 模块中。所有这些特性都涉及使用文件名,因此我们将从文件名类型开始。
18.2.1 OsStr 与 Path
麻烦的是,操作系统并不会强制要求其文件名是有效的 Unicode。下面是创建文本文件的两个 Linux shell 命令。第一个使用了有效的 UTF-8 文件名,第二个则没有:
$ echo "hello world" > ô.txt
$ echo "O brave new world, that has such filenames in't" > '\xf4'.txt
这两个命令都没有任何报错就通过了,因为 Linux 内核并不检查 UTF-8 的格式有效性。对内核来说,任意字节串(除了 null 字节和斜杠)都是可接受的文件名。在 Windows 上的情况类似:几乎任意 16 位“宽字符”字符串都是可接受的文件名,即使字符串不是有效的 UTF-16 也可以。操作系统处理的其他字符串也是如此,比如命令行参数和环境变量。 Rust 字符串始终是有效的 Unicode。文件名在实践中几乎总是 Unicode,但 Rust 必须以某种方式处理罕见的例外情况。这就是 Rust 会有 std::ffi::OsStr 和 OsString 的原因。 OsStr 是一种字符串类型,它是 UTF-8 的超集。OsStr 的任务是表示当前系统上的所有文件名、命令行参数和环境变量,无论它们是不是有效的 Unicode。在 Unix 上,OsStr 可以保存任意字节序列。在 Windows 上,OsStr 使用 UTF-8 的扩展格式存储,可以对任意 16 位值序列(包括不符合标准的半代用区码点)进行编码。所以我们有两种字符串类型:str 用于实际的 Unicode 字符串,而 OsStr 用于操作系统可能抛出的任意文字。还有用于文件名的 std::path::Path,这纯粹是一个便捷名称。Path 与 OsStr 完全一样,只是添加了许多关于文件名的便捷方法,18.2.2 节会介绍这些方法。绝对路径和相对路径都使用 Path 表示。对于路径中的单个组件,请使用 OsStr。 后,每种字符串类型都有对应的拥有型版本:String 拥有分配在堆上的 str,std::ffi:: OsString 拥有分配在堆上的 OsStr,而 std::path::PathBuf 拥有分配在堆上的 Path。表 18-1 概述了每种类型的一些特性。 表 18-1:文件名类型
| str | OsStr | Path | |
|---|---|---|---|
| 无固定大小类型,总是通过引用传递 | 是 | 是 | 是 |
| 可以包含任意 Unicode 文本 | 是 | 是 | 是 |
| 通常看起来和UTF-8一样 | 是 | 是 | 是 |
| 可以包含非 Unicode 数据 | 否 | 是 | 是 |
| 文本处理类方法 | 是 | 否 | 否 |
| 文件名相关方法 | 否 | 否 | 是 |
| 拥有型、可增长且分配在堆上的等价类型 | String | OsString | PathBuf |
| 转换为拥有型类型 | .to_string() | .to_os_string() | .to_path_buf() |
所有这 3 种类型都实现了一个公共特型 AsRef
use std::io;
use std::path::Path;
fn swizzle_file<P>(path_arg: P) -> io::Result<()>
where
P: AsRef<Path>,
{
let path = path_arg.as_ref();
}
所有接受 path 参数的标准函数和方法都使用了这种技巧,因此可以直接将字符串字面量传给它们中的任意一个。
18.2.2 Path 与 PathBuf 的方法
Path 提供了以下方法。 Path::new(str)(新建) 将 &str 或 &OsStr 转换为 &Path。这不会复制字符串。新的 &Path 会指向与原始 &str 或 &OsStr 相同的字节。
use std::path::Path;
let home_dir = Path::new("/home/fwolfe");
(类似的方法 OsStr::new(str) 会将 &str 转换为 &OsStr。) path.parent()(父目录) 返回路径的父目录(如果有的话)。返回类型是 Option<&Path>。 这不会复制路径。path 的父路径一定是 path 的子串。
assert_eq!(
Path::new("/home/fwolfe/program.txt").parent(),
Some(Path::new("/home/fwolfe"))
);
path.file_name()(文件名) 返回 path 的 后一个组件(如果有的话)。返回类型是 Option<&OsStr>。 典型情况下,path 由目录、斜杠和文件名组成,此方法会返回文件名。
use std::ffi::OsStr;
assert_eq!(
Path::new("/home/fwolfe/program.txt").file_name(),
Some(OsStr::new("program.txt"))
);
path.is_absolute()(是绝对路径?)和 path.is_relative()(是相对路径?) 这两个方法会指出文件是绝对路径(如 Unix 路径 /usr/bin/advent 或 Windows 路径 C:\Program Files)还是相对路径(如 src/main.rs)。 path1.join(path2)(联结) 联结两个路径,返回一个新的 PathBuf:
let path1 = Path::new("/usr/share/dict");
assert_eq!(path1.join("words"), Path::new("/usr/share/dict/words"));
如果 path2 本身是绝对路径,则只会返回 path2 的副本,因此该方法可用于将任意路径转换为绝对路径。
let abs_path = std::env::current_dir()?.join(any_path);
path.components()(组件迭代器) 返回从左到右访问给定路径各个组件的迭代器。这个迭代器的条 目类型是 std::path::Component,这是一个枚举,可以代表所有可能出现在文件名中的不同部分:
pub enum Component<'a> {
Prefix(PrefixComponent<'a>), // 驱动器路径或共享路径(在Windows
RootDir, // 根目录,`/`或`\`
CurDir, // 特殊目录`.`
ParentDir, // 特殊目录`..`
Normal(&'a OsStr), // 普通文件或目录名
}
例如,Windows 路径 \venice\Music\A Love Supreme\04Psalm.mp3 包含一个表示 \venice\Music 的 Prefix,后跟一个 RootDir,然后是表示 A Love Supreme 和 04-Psalm.mp3 的两个 Normal 组件。 有关详细信息,请参阅在线文档。 path.ancestors()(祖先迭代器) 返回一个从 path 开始一直遍历到根路径的迭代器。生成的每个条目也都是 Path:首先是 path 本身,然后是它的父级,接下来是它的祖父级,以此类推:
let file = Path::new("/home/jimb/calendars/calendar-18x18.pdf");
assert_eq!(
file.ancestors().collect::<Vec<_>>(),
vec![
Path::new(
"/home/jimb/calendars/calendar-
18x18.pdf"
),
Path::new("/home/jimb/calendars"),
Path::new("/home/jimb"),
Path::new("/home"),
Path::new("/")
]
);
这就像在重复调用 parent 直到它返回 None。 后一个条目始终是根路径或前缀路径(Prefix)。 这些方法只针对内存中的字符串进行操作。Path 也有一些能查询文件系统的方法:.exists()、.is_file()、.is_dir()、.read_dir()、. canonicalize() 等。请参阅在线文档以了解更多信息。 将 Path 转换为字符串有以下 3 个方法,每个方法都容许 Path 中存在无效 UTF-8。 path.to_str()(转字符串) 将 Path 转换为字符串,返回 Option<&str>。如果 path 不是有效的 UTF-8,则返回 None。
if let Some(file_str) = path.to_str() {
println!("{}", file_str);
} // ……否则就跳过这种名称古怪的文件
path.to_string_lossy()(转字符串,宽松版)
基本上和上一个方法一样,但该方法在任何情况下都会设法返回某种字符串。如果 path 不是有效的 UTF-8,则该方法会制作一个副本,用 Unicode 代用字符 替代每个无效的字节序列。
返回类型为 std::borrow::Cow
println!("Download found. You put it in: {}", dir_path.display());
此方法返回的值不是字符串,但它实现了 std::fmt::Display,因此可以与 format!()、println!() 和类似的宏一起使用。如果路径不是有效的 UTF-8,则输出可能包含 字符。
18.2.3 访问文件系统的函数
表 18-2 展示了 std::fs 中的一些函数及其在 Unix 和 Windows 上的近乎等价方式。所有这些函数都会返回 io::Result 值。除非另行说明,否则它们的返回值都是 Result<()>。 表 18-2:文件系统访问函数汇总表
| | Rust函数 | Unix | Windows |
| — | — | — | — |
| 创建和删除 | create_dir(path) create_dir_all(path) remove_dir(path) remove_dir_all(path) remove_file(path) | mkdir() 类似 mkdir -p rmdir() 类似 rm -r unlink() | CreateDirectory() 类似 mkdir RemoveDirectory() 类似 rmdir /s DeleteFile() |
| 复制、移动和链接 | copy(src_path, dest_path) -> Result
(copy() 返回的数值是已复制文件的大小,以字节为单位。要创建符号链接,请参阅 18.2.5 节。) 如你所见,Rust 会努力提供可移植的函数,这些函数可以在 Windows 以及 macOS、Linux 和其他 Unix 系统上如预期般工作。 关于文件系统的完整教程超出了本书的范畴,如果你对这些函数中的任何一个感到好奇,可以轻松地在网上找到更多相关信息。18.2.4 节中会展示一些示例。 所有这些函数都是通过调用操作系统实现的。例如, std::fs::canonicalize(path) 不仅会使用字符串处理来从给 定的 path 中消除 . 和 ..,还会使用当前工作目录解析相对路径,并追踪符号链接。如果路径不存在,则会报错。 std::fs::metadata(path) 和 std::fs::symlink_metadata(path) 生成的 Metadata 类型包含文件类型和大小、权限、时间戳等信息。同样,可以参阅在线文档了解详细信息。 为便于使用,Path 类型将其中一些内置成了方法,比如, path.metadata() 和 std::fs::metadata(path) 是一样的。
18.2.4 读取目录
要列出目录的内容,请使用 std::fs::read_dir 或 Path 中的等效方法 .read_dir():
for entry_result in path.read_dir()? {
let entry = entry_result?;
println!("{}", entry.file_name().to_string_lossy());
}
注意,在这段代码中有两行用到了 ? 运算符。第 1 行检查了打开目录时的错误。第 2 行检查了读取下一个条目时的错误。 entry 的类型是 std::fs::DirEntry,这个结构体提供了数个方法。 entry.file_name()(文件名) 文件或目录的名称,是 OsString 类型的。 entry.path()(路径) 与 entry.file_name() 基本相同,但 entry.path() 联结了原始路径,生成了一个新的 PathBuf。如果正在列出的目录是 “/home/jimb”,并且 entry.file_name() 是 “.emacs”,那么 entry.path() 将返回
PathBuf::from("/home/jimb/.emacs").
entry.file_type()(文件类型)
返回 io::Result
use std::fs;
use std::io;
use std::path::Path;
/// 把现有目录`src`复制到目标路径`dst`
fn copy_dir_to(src: &Path, dst: &Path) -> io::Result<()> {
if !dst.is_dir() {
fs::create_dir(dst)?;
}
for entry_result in src.read_dir()? {
let entry = entry_result?;
let file_type = entry.file_type()?;
copy_to(&entry.path(), &file_type, &dst.join(entry.file_name()))?;
}
Ok(())
}
独立的 copy_to 函数用于复制单个目录条目。
/// 把`src`中的任何内容复制到目标路径`dst`
fn copy_to(src: &Path, src_type: &fs::FileType, dst: &Path) -> io::Result<()> {
if src_type.is_file() {
fs::copy(src, dst)?;
} else if src_type.is_dir() {
copy_dir_to(src, dst)?;
} else {
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"don't know how to copy:
{}",
src.display()
),
));
}
Ok(())
}
18.2.5 特定于平台的特性
到目前为止,copy_to 函数既可以复制文件也可以复制目录。接下来我们还打算在 Unix 上支持符号链接。 没有可移植的方法来创建同时适用于 Unix 和 Windows 的符号链接,但标准库提供了一个特定于 Unix 的 symlink 函数:
use std::os::unix::fs::symlink;
有了这个函数,我们的工作就很容易了。只需向 copy_to 中的 if 表达式添加一个分支即可:
...
} else if src_type.is_symlink() { let target = src.read_link()?; symlink(target, dst)?; ...
如果只是为 Unix 系统(如 Linux 和 macOS)编译我们的程序,那么就能这么用。 std::os 模块包含各种特定于平台的特性,比如 symlink。 std::os 在标准库中的实际主体如下所示(这里为了看起来整齐,调整了代码格式):
//! 特定于操作系统的功能
#[cfg(unix)] pub mod unix; #[cfg(windows)] pub mod windows; #[cfg(target_os = “ios”)] pub mod ios; #[cfg(target_os = “linux”)] pub mod linux; #[cfg(target_os = “macos”)] pub mod macos;
#[cfg] 属性表示条件编译:这些模块中的每一个仅在某些平台上存在。这就是为什么使用 std::os::unix 修改后的程序只能针对 Unix 成功编译,因为在其他平台上 std::os::unix 不存在。 如果希望代码在所有平台上编译,并支持 Unix 上的符号链接,则必须也在程序中使用 #[cfg]。在这种情况下, 简单的方法是在 Unix 上导入 symlink,同时在其他系统上定义自己的 symlink 模拟实现: #[cfg(unix)]
use std::os::unix::fs::symlink;
/// 在未提供`symlink`的平台上提供的模拟实现
#[cfg(not(unix))]
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, _dst: Q) -> std::io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Other,
format!("can't copy symbolic link: {}", src.as_ref().display()),
))
}
事实上,symlink 只是特殊情况。大多数特定于 Unix 的特性不是独立函数,而是将新方法添加到标准库类型的扩展特型(11.2.2 节介绍过扩展特型)。prelude 模块可用于同时启用所有这些扩展:
use std::os::unix::prelude::*;
例如,在 Unix 上,这会向 std::fs::Permissions 添加 .mode() 方法,从而支持表达 Unix 权限所需的底层 u32 值。同样,这还会给 std::fs::Metadata 添加一些访问器方法,从而得以访问底层 struct stat 的字段(比如 .uid() 可获得文件所有者的用户 ID)为 std::fs::Metadata 添加了访问器。 总而言之,std::os 中的内容非常基础。更多特定于平台的功能可通过第三方 crate 获得,比如用于访问 Windows 注册表的 winreg。
18.3 网络
关于网络的教程远远超出了本书的范畴。但是,如果你已经对网络编程有所了解,那么本节能帮你开始使用 Rust 中的网络支持。 要编写较底层的网络代码,可以使用 std::net 模块,该模块为 TCP 网络和 UDP 网络提供了跨平台支持。可以使用 native_tls crate 来支持 SSL/TLS。 这些模块为网络上直接的、阻塞型的输入和输出提供了一些基础构 件。用几行代码就可以编写一个简单的服务器,只要使用 std::net 并为每个连接启动一个线程即可。例如,下面是一个“回显” (echo)服务器:
use std::io;
use std::net::TcpListener;
use std::thread::spawn;
/// 不断接受连接,为每个连接启动一个线程
fn echo_main(addr: &str) -> io::Result<()> {
let listener = TcpListener::bind(addr)?;
println!("listening on {}", addr);
loop {
// 等待客户端连入
let (mut stream, addr) = listener.accept()?;
println!("connection received from {}", addr);
// 启动一个线程来处理此客户端
let mut write_stream = stream.try_clone()?;
spawn(move || {
// 回显从`stream`中收到的一切 io::copy(&mut stream, &mut write_stream) .expect("error in client thread: "); println!("connection closed");
});
}
}
fn main() {
echo_main("127.0.0.1:17007").expect("error: ");
}
回显服务器会简单地重复发给它的所有内容。这种代码与用 Java 或 Python 编写的代码没有太大区别。(第 19 章会介绍 std::thread::spawn()。) 但是,对于高性能服务器,需要使用异步输入和输出。第 20 章会介绍 Rust 对异步编程的支持,并展示网络客户端和服务器的完整代码。 更高层级的协议由第三方 crate 提供支持。例如,reqwest crate 为 HTTP 客户端提供了一个漂亮的 API。下面是一个完整的命令行程序,该程序可以通过 http: 或 https: URL 获取对应文档,并将其内容打印到你的终端。此代码是使用 reqwest = “0.11” 编写的,并启用了其 “blocking” 特性。reqwest 还提供了一个异步接口。 use std::error::Error; use std::io;
fn http_get_main(url: &str) -> Result<(), Box
// 读取响应体并写到标准输出
let stdout = io::stdout(); io::copy(&mut response, &mut stdout.lock())?;
Ok(())
}
fn main() { let args: Vec
if let Err(err) = http_get_main(&args[1]) { eprintln!(“error: {}”, err);
}
}
actix-web 框架为 HTTP 服务器提供了一些高层次抽象,比如
Service 特型和 Transform 特型,这两个特型可以帮助你从一些可插接部件组合出应用程序。websocket crate 实现了 WebSocket 协议。Rust 是一门年轻的语言,拥有繁荣的开源生态系统,对网络的支持正在迅速扩展。