输入与输出
Rust 标准库中的输入和输出的特性是围绕 3 个特型组织的,即 Read
、 BufRead
和 Write
。
- 实现了 Read 的值具有面向字节的输入方法。它们叫作读取器。
- 实现了 BufRead 的值是缓冲读取器。它是 Read的子特型 ,外加读取文本行等方法。
- 实现了 Write 的值能支持面向字节和 UTF-8 文本的输出。它们叫作写入器。
读取器与写入器
读取器和写入器都有标准特型(std::io::Read
和 std::io::Write
),因此编写适用于各种输入通道或输出通道的泛型代码是很常见的。
将所有字节从任意读取器复制到任意写入器的函数:
rust">use std::io::{self, Read, Write, ErrorKind};
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…
std::io 的 Read、BufRead 和 Write 这 3 个特型以及 Seek 非常常用,下 面是一个只包含这些特型的 prelude 模块:use std::io::prelude::*;
导入 std::io 模块本身: use std::io::{self, Read, Write, ErrorKind};
self 关键字将 io 声明成了 std::io
模块的别名。这样, std::io::Result
和 std::io::Error
就可以更简洁地写为 io::Result
和 io::Error
了。
读取器
std::io::Read
有以下几个读取数据的方法。所有这些方法都需要对读取器本身进行可变引用。
-
reader.read(&mut buffer)
(读取) 从数据源中读取一些字节并将它们存储在给定的 buffer 中。buffer 参数 的类型是 &mut[u8]。此方法最多会读取 buffer.len() 字节。 返回类型是 io::Result,它是 Result 的类型别名。成功时,这个 u64 的值是已读取的字节数——可能等于或小于 buffer.len(),就算数据源突然涌入更多数据也不会超出。-
返回Ok(0) 则意味着 没有更多输入可以读取了。
-
出错时,.read() 会返回 Err(err),其中 err 是一个 io::Error 值。
io::Error 是可打印的;为了便于程序处理,它有一个 .kind() 方法,该方法会返回
io::ErrorKind
类型的错误代码。此枚举的成员都有PermissionDenied 和 ConnectionReset
之类的名称。大多数表示 严重的错误,不容忽视,但有一种错误需要特殊处理。io::ErrorKind::Interrupted
对应于 Unix 错误码 EINTR,表示读取恰好被某种信号中断了。除非程序的设计目标之一就是对信号中断做一些巧妙的处 理,否则就应该再次尝试读取。.read() 方法是非常底层的,甚至还继承了底层操作系统的某些怪癖。如果你正在为一种新型数据源实现 Read 特型,那么这会给你很大的发 挥空间。但如果你想读取一些数据,则会很痛苦。因此,Rust 提供了几个更高级的便捷方法。这些方法是 Read 中的默认实现,而且都处理了
ErrorKind::Interrupted
,这样你就不必自己处理了。
-
-
reader.read_to_end(&mut byte_vec)
(读至末尾) 从这个读取器读取所有剩余的输入,将其追加到 Vec 型的 byte_vec 中。返回 io::Result,即已读取的字节数。 此方法对要装入向量中的数据量没有任何限制,因此不要在不受信任的来源上使用它。(可以用接下来要讲的 .take() 方法加以限制。) -
reader.read_to_string(&mut string)
(读取字符串) 和上一个方法类似,但此方法会将数据附加到给定的 String。如果流不是 有效的 UTF-8,则返回 ErrorKind::InvalidData 错误。 在某些编程语言中,字节输入和字符输入是由不同的类型处理的。如今, UTF-8 占据了绝对主导地位,所以 Rust 承认这一事实标准并且处处支持 UTF8。开源的encoding crate
可以支持其他字符集。 -
reader.read_exact(&mut buf)
(精确读满) 读取足够的数据来填充给定的缓冲区。参数类型是 &[u8]。如果读取器在 读够 buf.len() 字节之前就耗尽了数据,那么此方法就会返回ErrorKind::UnexpectedEof
错误。
此外,还有 3 个适配器方法可以按值获取 reader,将其转换为迭代器或另一种读取器。
reader.bytes()
(字节迭代器) 返回输入流中各字节的迭代器。条目类型是io::Result
,因此每字 节都需要进行错误检查。此外,此方法会为每字节调用一次 reader.read(), 如果没有缓冲,则会非常低效。reader.chain(reader2)
(串联) 返回新的读取器,先生成来自 reader 的所有输入,然后再生成来自 reader2 的所有输入。reader.take(n)
(取出 n 个) 返回新的读取器,从与 reader 相同的数据源读取,但仅限于前 n 字节的 输入。 没有关闭读取器的方法。读取器和写入器通常会实现 Drop 以便自行关闭。
缓冲读取器
为了提高效率,可以对读取器和写入器进行缓冲,这基本上意味着它们有一块内存(缓冲区),用于保存一些输入数据或输出数据。这可以减少一些系统调用, 如图应用程序会从 BufReader 中读取数据,在本例中是通过调用 其 .read_line() 方法实现的。BufReader 会依次从操作系统获取更大块的输入。
BufReader 缓冲区的实际大小默认为几千字节,因此 一次 read 系统调用就可以服务数百次 .read_line()
调用。这很重要,因为系统调用很慢。
操作系统也有自己的缓冲区。同理:系统调用固然慢,但从 磁盘读取数据更慢。
缓冲读取器同时实现了 Read 特型和 BufRead 特型,后者添加了以下方法:
reader.read_line(&mut line)
(读一行) 读取一行文本并将其追加到 line,line 是一个 String。行尾的换行符 ‘\n’ 包含在 line 中。 如果输入带有 Windows 风格的行尾结束符号 “\r\n”,则这两个字符都会 包含在 line 中。 返回值是io::Result
,表示已读取的字节数,包括行尾结束符 号(如果有的话)。 如果读取器在输入的末尾,则会保持 line 不变并返回 Ok(0)。reader.lines()
(文本行迭代器) 返回生成各个输入行的迭代器。条目类型是io::Result
。字符串中不包含换行符。如果输入带有 Windows 风格的行尾结束符号 “\r\n”,则这两个字符都会被去掉。 在绝大多数场景中,这个方法足够你进行文本输入了。reader.read_until(stop_byte, &mut byte_vec)
(读到 stop_byte 为止)和reader.split(stop_byte)
(根据 stop_byte 拆分) 与 .read_line() 和 .lines() 类似,但这两个方法是面向字节的,会生成 Vec 而不是 String。你要自选分隔符 stop_byte。- BufRead 还提供了
.fill_buf()
和.consume(n)
,这是一对底层方法,用于直接访问读取器的内部缓冲区。
读取行
用于实现 Unix grep
实用程序的函数,该函数会在多行文本(通常 是通过管道从另一条命令输入的文本)中搜索给定字符串:【现在仅支持终端搜索】
rust">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 程序,让它支持在磁盘上搜索文件。可以把这个函数 变成泛型函数:
rust">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 传给它
rust">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程序。
rust">// 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())?; //grep终端} else {for file in files { //grep filelet f = File::open(file)?;grep(&target, BufReader::new(f))?; //drop自动关闭file_fd}} Ok(())
}fn main() {let result = grep_main();if let Err(err) = result {eprintln!("{}", err);std::process::exit(1);}
}
收集行
有些读取器方法(包括 .lines())会返回生成 Result 值的迭代器。当你第一次想要将文件的所有行都收集到一个大型向量中时,就会遇到如何摆脱 Result 的问题
rust">// 正确,但不是你想要的
let results: Vec<io::Result<String>> = reader.lines().collect();// 错误:不能把Result的集合转换成Vec<String>
let lines: Vec<String> = reader.lines().collect();
- 该迭代器的每个元素是一个
Result<String>
类型。- 使用
collect()
方法将这些Result<String>
收集到一个Vec<io::Result<String>>
中。- 每个元素仍然是一个
Result<String>
,这意味着你需要进一步处理这些结果。
第二次尝试无法编译:遇到这些错误怎么办?最直观的解决方法是编写一个 for 循环并检查每个条目是否有错:
rust">let mut lines = vec![];
for line_result in reader.lines() {lines.push(line_result?);
}
这固然没错,但这里最好还是用 .collect(),事实上确实可以做到。只要知道该请求哪种类型就可以了:
rust">let lines = reader.lines().collect::<io::Result<Vec<String>>>()?;
这是怎么做到的呢?标准库中包含了 Result 对 FromIterator
的实现,这个实现让一切成为可能:
rust">impl<T, E, C> FromIterator<Result<T, E>> for Result<C, E> where C: FromIterator<T>
{...
}
假设 C 是任意集合类型,比如 Vec 或 HashSet。只要已经知道如何从 T 值的迭代器构建出 C,就可以从生成 Result<T,E>
值的迭代器构建出 Result<C,E>
。只需从迭代器中提取各个 值并从 Ok 结果构建出集合即可,但一旦看到 Err,就停止并将其传出。
换句话说,io::Result<Vec<String>>
是一种集合类型,因此 .collect()
方法可以创建并填充该类型的值。
也可以:
使用 filter_map
或者 map
和 collect
结合使用
方法一:使用 filter_map
rust">use std::fs::File;
use std::io::{self, BufRead};fn main() -> io::Result<()> {let file = File::open("example.txt")?;let reader = io::BufReader::new(file);let lines: Vec<String> = reader.lines().filter_map(|line| line.ok()).collect();println!("{:?}", lines);Ok(())
}
filter_map
方法会过滤掉所有Err
值,并将Ok
值映射为其内部值。line.ok()
将Result<String>
转换为Option<String>
,其中Ok(value)
变为Some(value)
,而Err(_)
变为None
。filter_map
会自动过滤掉所有的None
值,只保留Some
值。
方法二:使用 map
和 collect
rust">use std::fs::File;
use std::io::{self, BufRead};fn main() -> io::Result<()> {let file = File::open("example.txt")?;let reader = io::BufReader::new(file);let lines: Vec<String> = reader.lines().map(|result| result.unwrap_or_else(|_| String::from(""))).collect();println!("{:?}", lines);Ok(())
}
map
方法会对每个Result<String>
应用一个闭包。result.unwrap_or_else(|_| String::from(""))
会处理Result
,如果成功则返回String
,如果失败则返回一个空字符串。collect
方法将处理后的结果收集到一个Vec<String>
中。
写入器
输入主要是用方法完成的,而输出略有不同
要将输出发送到写入器,请使用 write!() 宏和 writeln!() 宏。它们和 print!() 和 println!() 类似,但有两点区别:
- 一是每个 write 宏都接受一个额外的写入器作为第一参数。
- 二是它们会返回 Result,因此必须处理错误。这就是为什么要在每行末尾使用 ? 运算符。 print 宏不会返回 Result,如果写入失败,它们只会 panic。由于写入的是终 端,所以极少失败。
Write 特型有以下几个方法
writer.write(&buf)
(写入) 将切片 buf 中的一些字节写入底层流。此方法会返回io::Result
。成功时,这给出了已写入的字节数,如果流突然提前关闭,那么这个值可能会小于buf.len()
。 和 Reader::read() 一样,这是一个要避免直接使用的底层方法。writer.write_all(&buf)
(写入全部) 将切片 buf 中的所有字节都写入。返回 Result<()>。writer.flush()
(刷新缓冲区) 将可能被缓冲在内存中的数据刷新到底层流中。返回 Result<()>。 请注意,虽然 println! 宏和 eprintln! 宏会自动刷新 stdout 流和 stderr 流的缓冲区,但 print! 宏和 eprint! 宏不会。使用它们时,可能要 手动调用 flush()。 与读取器一样,写入器也会在被丢弃时自动关闭。- 正如
BufReader::new(reader)
会为任意读取器添加缓冲区一样,BufWriter::new(writer)
也会为任意写入器添加缓冲区 - 要设置缓冲区的大小,请使用
BufWriter::with_capacity(size, writer)
。
当丢弃 BufWriter 时,所有剩余的缓冲数据都将写入底层写入器。但是,如果在此写入过程中发生错误,则错误会被忽略。(由于错误发生在 BufWriter 的 .drop() 方法内部,因此没有合适的地方来报告。)为了确保应用程序会注意 到所有输出错误,请在丢弃带缓冲的写入器之前将它手动 .flush() 一下。
文件
打开文件的两个方法:
File::open(filename)
(打开) 打开现有文件进行读取。此方法会返回一个io::Result
,如果该 文件不存在则报错。File::create(filename)
(创建) 创建一个用于写入的新文件。如果存在具有给定文件名的文件,则会将其截 断。
请注意,File 类型位于文件系统模块 std::fs
中,而不是 std::io
中。 当这两个方法都不符合需求时,可以使用 OpenOptions
来指定所期望的确切行为:
rust">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
。File 打开后的行为就和任何读取器或写入器一样。如果需要,可以为它添加缓冲区。File 在被丢弃时会自动关闭。
寻址
File 还实现了 Seek 特型,这意味着你可以在 File 中“跳来跳去”,而不是从 头到尾一次性读取或写入。Seek 的定义如下
rust">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),每一次寻址的开销都接近于读取数兆字节的数据。
其他读取器与写入器
迄今为止,本章一直在使用 File 作为示范的主力,但还有许多其他有用的读取 器类型和写入器类型。
-
io::stdin()
(标准输入) 返回标准输入流的读取器。类型是 io::Stdin。由于它被所有线程共享, 因此每次读取都会获取和释放互斥锁。 Stdin 有一个 .lock() 方法,该方法会获取互斥锁并返回io::StdinLock
,这是一个带缓冲的读取器,在被丢弃之前会持有互斥锁。因此,对 StdinLock 的单个操作就避免了互斥开销。Rust 1.8 开始,已经可以直接调用 io::stdin().lock()rust">let stdin = io::stdin(); let lines = stdin.lock().lines();
-
io::stdout()
(标准输出)和io::stderr()
(标准错误) 返回标准输出流(Stdout)类型和标准错误流(Stderr)类型的写入 器。它们也有互斥锁和 .lock() 方法。 -
Vec<u8>
(u8 向量) 实现了 Write。写入Vec<u8>
会使用新数据扩展向量。 (但是,String 没有实现 Write。要使用 Write 构建字符串,需要首先 写入 Vec,然后使用String::from_utf8(vec)
将向量转换为字符串) -
Cursor::new(buf)
(新建) 创建一个Cursor
(游标,一个从 buf 读取数据的缓冲读取器)。这样你 就创建了一个能读取 String 的读取器。参数 buf 可以是实现了AsRef<[u8]>
的任意类型,因此也可以传递 &[u8]、&str 或Vec<u8>
。Cursor 的内部平平无奇,只有两个字段:buf 本身和一个整数,该整数是 buf 中下一次读取开始的偏移量。此位置的初始值为 0。
Cursor 实现了 Read、BufRead 和 Seek。如果 buf 的类型是
&mut [u8]
或Vec<u8>
,那么 Cursor 也实现了 Write。写入游标会覆盖 buf 中 从当前位置开始的字节。如果试图直接写到超出 &mut [u8] 末尾的位置,就会导致一次“部分写入”或一个 io::Error。不过,使用游标写入 Vec 的结尾就没有这个问题:它会增长此向量。因此,Cursor<&mut [u8]>
和Cursor<Vec<u8>>
实现了所有这 4 个std::io::prelude
特型。 -
std::net::TcpStream
(Tcp 流) 表示 TCP 网络连接。由于 TCP 支持双向通信,因此它既是读取器又是写入器。 类型关联函数TcpStream::connect(("hostname", PORT))
会尝试 连接到服务器并返回io::Result<TcpStream>
。 -
std::process::Command
(命令)支持启动子进程并通过管道将数据传输到其标准输入:
rust">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 的类型是
Option<std::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)
(重复) 返回一个会无限重复给定字节的读取器。
二进制数据和序列化
许多开源 crate 建立在 std::io
框架之上,以提供额外的特性。
1、byteorder crate
提供了 ReadBytesExt 特型和 WriteBytesExt 特型,为所有读取器和写入器添加了二进制输入和输出的方法:
rust">use byteorder::{ReadBytesExt, WriteBytesExt, LittleEndian};
let n = reader.read_u32::<LittleEndian>()?;
writer.write_i64::<LittleEndian>(n as i64)?;
2、flate2 crate
提供了用于读取和写入 gzip 数据的适配器方法:
rust">use flate2::read::GzDecoder;
let file = File::open("access.log.gz")?;
let mut gzip_reader = GzDecoder::new(file);
3、serde crate
及其关联的格式类 crate(如 serde_json)实现了序列化和反序列化:它们在 Rust 结构体和字节之间来回转换。
有一些数据(文字冒险游戏中的映射表)存储在 HashMap 中:
rust">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 或任意写入器。前面的代码会将数据打印到 stdout 为:
rust">{"Debris Room":[["E","Cobble Crawl"],["W","Sloping Canyon"]],"Cobble Crawl":
[["W","Debris Room"]]}
serde 还包括对供派生的两个关键 serde 特型的支持:
rust">#[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"
简而言之,构建系统会为 struct Player
自动生 成 serde::Serialize 和 serde::Deserialize 的实现,因此序列化 Player 的值很简单:
serde_json::to_writer(&mut std::io::stdout(), &player)?;
输出:
{"location":"Cobble Crawl","items":["a wand"],"health":3}
文件与目录
文件和目录的特性位于 std::path 模块和 std::fs 模块
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 完全一样,只是添加了许多关于文件名的便捷方法。绝对路径和相对路径都使用 Path 表示。 对于路径中的单个组件,请使用 OsStr。 - 最后,每种字符串类型都有对应的拥有型版本:String 拥有分配在堆上的 str,
std::ffi:: OsString
拥有分配在堆上的OsStr
,而std::path::PathBuf
拥有分配在堆上的 Path。
所有这 3 种类型都实现了一个公共特型 AsRef
,因此我们可以轻松地声明一个接受“任意文件名类型”作为参数的泛型函数。
rust">use std::path::Path;
use std::io;
fn swizzle_file<P>(path_arg: P) -> io::Result<()>where P: AsRef<Path>
{let path = path_arg.as_ref();...
}
Path 与 PathBuf 的方法
-
Path::new(str)
(新建) 将 &str 或 &OsStr 转换为 &Path。这不会复制字符串。新的 &Path 会 指向与原始 &str 或 &OsStr 相同的字节。[类似的方法OsStr::new(str)
会将 &str 转换为 &OsStr]rust">use std::path::Path; let home_dir = Path::new("/home/fwolfe");
-
path.parent()
(父目录) 返回路径的父目录(如果有的话)。返回类型是Option<&Path>
。 这不会复制路径。path 的父路径一定是 path 的子串。 -
path.file_name()
(文件名) 返回 path 的最后一个组件(如果有的话)。返回类型是 Option<&OsStr>。 典型情况下,path 由目录、斜杠和文件名组成,此方法会返回文件名。 -
path.is_absolute()
(是绝对路径?)和path.is_relative()
(是 相对路径?) 这两个方法会指出文件是绝对路径(如 Unix 路径 /usr/bin/advent 或 Windows 路径 C:\Program Files)还是相对路径(如 src/main.rs)。 -
path1.join(path2)
(联结) 联结两个路径,返回一个新的 PathBuf:rust">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
,这是一个枚举,可以代表所有可能出现在文件名中 的不同部分:rust">pub enum Component<'a> {Prefix(PrefixComponent<'a>), // 驱动器路径或共享路径(在Windows 上)RootDir, // 根目录,`/`或`\`CurDir, // 特殊目录`.`ParentDir, // 特殊目录`..`Normal(&'a OsStr) // 普通文件或目录名 }
-
path.ancestors()
(祖先迭代器) 返回一个从 path 开始一直遍历到根路径的迭代器。生成的每个条目也都是 Path:首先是 path 本身,然后是它的父级,接下来是它的祖父级,以此类推:rust">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("/")]);
这些方法只针对内存中的字符串进行操作。Path 也有一些能查询文件系统的方 法:.exists()、.is_file()、.is_dir()、.read_dir()、.canonica lize()
等。
将 Path 转换为字符串有以下 3 个方法,每个方法都容许 Path 中存在无效 UTF-8。
-
path.to_str()
(转字符串) 将 Path 转换为字符串,返回 Option<&str>。如果 path 不是有效的 UTF-8,则返回 None。 -
path.to_string_lossy()
(转字符串,宽松版) 基本上和上一个方法一样,但该方法在任何情况下都会设法返回某种字符 串。如果 path 不是有效的 UTF-8,则该方法会制作一个副本,用 Unicode 代用字符 � 替代每个无效的字节序列。返回类型为std::borrow::Cow<str>
:借用或拥有的字符串。要从此值 获取 String,请使用其 .to_owned() 方法。 -
path.display()
(转显示) 用于打印路径:println!("Download found. You put it in: {}", dir_path.display());
此方法返回的值不是字符串,但它实现了std::fmt::Display
,因此可 以与 format!()、println!() 和类似的宏一起使用。如果路径不是有效的 UTF-8,则输出可能包含 � 字符。
访问文件系统的函数
这些函数都会返回 io::Result
值。除非另行说明,否则它们的返回值都是 Result<()>
。
所有这些函数都是通过调用操作系统实现的。
例如, std::fs::canonicalize(path)
不仅会使用字符串处理来从给定的 path 中消除 . 和 ..
,还会使用当前工作目录解析相对路径,并追踪符号链接。如果路径不存在,则会报错。
std::fs::metadata(path)
和 std::fs::symlink_metadata(path)
生成的 Metadata 类型包含文件类型和大小、权限、时间戳等信息。
为便于使用,Path 类型将其中一些内置成了方法,比如 path.metadata()
和 std::fs::metadata(path)
是一样的。
读取目录
要列出目录的内容,请使用 std::fs::read_dir
或 Path 中的等效方法 .read_dir()
:
rust">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<FileType>
。FileType 有
.is_file()
方 法、.is_dir()
方法和.is_symlink()
方法。 -
entry.metadata()
(元数据) 获取有关此条目的其他元数据。特殊目录. 和 ..
在读取目录时不会列出。
递归地将目录树从磁盘上的一个位置复制到另一个位置:
rust">use std::fs;
use std::io;
use std::path::Path;/// 把`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(())
}/// 把现有目录`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 函数既可以复制文件也可以复制目录。接下来我们还打 算在 Unix 上支持符号链接。 没有可移植的方法来创建同时适用于 Unix 和 Windows 的符号链接,但标准库提 供了一个特定于 Unix 的 symlink
函数:
use std::os::unix::fs::symlink;
copy_to 中的 if 表达式添加 一个分支即可:
rust">...
} else if src_type.is_symlink() {let target = src.read_link()?;symlink(target, dst)?;
...
如果只是为 Unix 系统(如 Linux 和 macOS)编译我们的程序,那么就能这么用。
std::os 模块包含各种特定于平台的特性,比如 symlink
。std::os 在标准 库中的实际主体如下所示(这里为了看起来整齐,调整了代码格式):
rust">//! 特定于操作系统的功能
#[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 模拟实现:
rust">#[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 的特性不是独立函数,而是将新方法添加到标准库类型的扩展特型
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
。
网络
要编写较底层的网络代码,可以使用 std::net
模块,该模块为 TCP 网络和 UDP 网络提供了跨平台支持。
可以使用 native_tls crate
来支持 SSL/TLS。
这些模块为网络上直接的、阻塞型的输入和输出提供了一些基础构件。用几行代码就可以编写一个简单的服务器,只要使用 std::net
并为每个连接启动一个线程即可。
下面是一个“回显”(echo)服务器:
rust">use std::net::TcpListener;
use std::io;
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: ");
}
回显服务器会简单地重复发给它的所有内容
对于高性能服务器,需要使用异步输入和输出
更高层级的协议由第三方 crate 提供支持。
例如,reqwest crate
为 HTTP 客户 端提供了一个漂亮的 API。下面是一个完整的命令行程序,该程序可以通过 http: 或 https: URL 获取对应文档,并将其内容打印到你的终端。此代码是使用 reqwest = “0.11” 编写的,并启用了其 “blocking” 特性。reqwest 还提供了一个异步接口。
rust">use std::error::Error;
use std::io;fn http_get_main(url: &str) -> Result<(), Box<dyn Error>> {// 发送HTTP请求并获取响应let mut response = reqwest::blocking::get(url)?;if !response.status().is_success() {Err(format!("{}", response.status()))?;}// 读取响应体并写到标准输出let stdout = io::stdout();io::copy(&mut response, &mut stdout.lock())?;Ok(())
}
fn main() {let args: Vec<String> = std::env::args().collect();if args.len() != 2 {eprintln!("usage: http-get URL");return;}if let Err(err) = http_get_main(&args[1]) {eprintln!("error: {}", err); }
}
actix-web
框架为 HTTP 服务器提供了一些高层次抽象,比如 Service
特型 和 Transform
特型,这两个特型可以帮助你从一些可插接部件组合出应用程序。
websocket crate
实现了 WebSocket 协议。
…