输入和输出(Input and Output)

杜利特尔:你有什么具体证据表明你存在吗? 炸弹 #20:嗯…嗯……我思故我在. 杜利特尔:很好.非常好.但是你怎么知道别的东西存在呢? 炸弹 #20:我的感官装置向我展示了它. —Dark Star

原文

Doolittle: What concrete evidence do you have that you exist? Bomb #20: Hmmmm…well…I think, therefore I am. Doolittle: That’s good. That’s very good. But how do you know that anything else exists? Bomb #20: My sensory apparatus reveals it to me. —Dark Star

Rust的输入和输出标准库功能围绕三个trait组织—Read,BufReadWrite—以及实现它们的各种类型:

  • 实现Read的值具有面向字节的输入的方法.它们被称为 读取器(readers).

  • 实现BufRead的值是 缓冲(buffered) 读取器.它们支持Read的所有方法,还有读取文本行的方法等.

  • 实现Write的值支持面向字节和UTF-8文本输出.它们被称为 写入器(writers).

图18-1显示了这三个trait以及读写器类型的一些示例.

在本章中,我们将展示如何使用这些trait及其方法,各种实现它们的类型,以及与文件,终端和网络交互的其他方式.

图18-1. 从Rust标准库中选择的读取器和写入器类型.

读取器和写入器(Readers and Writers)

读取器(Readers) 是程序可以从中读取字节的值.例子包括:

  • 使用std::fs::File::open(filename)打开的文件.

  • std::net::TcpStream,用于通过网络接收数据.

  • std::io::stdin(),用于从进程的标准输入流中读取.

  • std::io::Cursor<&[u8]>值,它们是从已经在内存中的字节数组”读取(read)”的读取器.

写入器(Writers) 是程序可以写入字节的值.例子包括:

  • 使用std::fs::File::create(filename)打开的文件.

  • std::net::TcpStream,用于通过网络发送数据.

  • std::io::stdout()std::io::stderr(),用于写入终端.

  • std::io::Cursor<&mut [u8]>值,它允许你将任何可变的字节切片视为用于写入的文件.

  • Vec<u8>,一个writer方法附加到向量的写入器.

由于读取器和写入器有标准的traits(std::io::Readstd::io::Write),因此编写适用于各种输入或输出通道的泛型代码是很常见的.例如,这是一个将从任何读取器的所有字节复制到任何写入器器的函数:

  1. use std::io::{self, Read, Write, ErrorKind};
  2. const DEFAULT_BUF_SIZE: usize = 8 * 1024;
  3. pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W)
  4. -> io::Result<u64>
  5. where R: Read, W: Write
  6. {
  7. let mut buf = [0; DEFAULT_BUF_SIZE];
  8. let mut written = 0;
  9. loop {
  10. let len = match reader.read(&mut buf) {
  11. Ok(0) => return Ok(written),
  12. Ok(len) => len,
  13. Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
  14. Err(e) => return Err(e),
  15. };
  16. writer.write_all(&buf[..len])?;
  17. written += len asu64;
  18. }
  19. }

这是Rust标准库中std::io::copy()的实现.由于它是泛型的,你可以使用它将数据从File复制到TcpStream,从Stdin复制到内存中的Vec<u8>等.

如果此处的错误处理代码不清楚,请重新访问第7章.我们将在前面的页面中不断使用Result;掌握它们的工作方式非常重要.

四个std::iotraitRead,BufRead,WriteSeek是如此常用,以至于有个只包含这些trait的prelude模块:

  1. use std::io::prelude::*;

你会在本章中看到这一两次.我们还有导入std::io模块本身的习惯:

  1. use std::io::{self,Read,Write,ErrorKind};

self关键字在此声明iostd::io模块的别名. 这样,std::io::Resultstd::io::Error可以更简洁地编写为io::Resultio::Error,依此类推.

读取器(Readers)

std::io::Read有几种读取数据的方法.所有这些都通过mut引用来接受reader.

  • reader.read(&mut buffer)从数据源读取一些字节并将它们存储在给定的buffer中.buffer参数的类型是&mut [u8].这会读取buffer.len()个字节.

返回类型是io::Result<u64>,它是Result<u64,io::Error>的类型别名.成功时,u64值是读取的字节数—可能等于或小于buffer.len(), 即使有更多数据要来(even if there’s more data to come) ,数据源的一时兴起.Ok(0)表示没有更多的输入要读取.

出错时,.read()返回Err(err),其中errio::Error值.io::Error是可打印的,对于人类友好;对于程序,它有一个.kind()方法,它返回类型为io::ErrorKind的错误码.此枚举的成员有PermissionDeniedConnectionReset等名称.大多数表示不容忽视的严重错误,但应特别处理一种错误.io::ErrorKind::Interrupted对应于Unix错误码EINTR,这意味着读取恰好被信号中断.除非程序设计为使用信号做一些巧妙的事情,否则它应该重试读取.上一节中copy()的代码显示了一个这样的例子.

如你所见,.read()方法非常低级,甚至继承了底层操作系统的怪癖.如果你正在为新类型的数据源实现Readtrait,这会给你留下很多余地.如果你正在尝试读一些数据,那就很痛苦.因此,Rust提供了几种更高级的方便的方法.所有这些都有.read()的默认实现.它们都处理ErrorKind::Interrupted,所以你不必自己做了.

  • reader.read_to_end(&mut byte_vec)读取此读取器的所有剩余输入,并将其追加到byte_vec,这是一个Vec<u8>.返回io::Result<()>.此方法对将堆积到向量中的数据量没有限制,因此请勿在不受信任的源上使用它.(你可以使用.take()方法施加限制,如下所述.)

  • reader.read_to_string(&mut string)相同,但将数据追加到给定的String.如果流时无效的UTF-8,则返回ErrorKind::InvalidData错误.

在某些语言中,字节输入和字符输入由不同类型处理.如今,UTF-8占据主导地位,Rust承认这一事实上的标准,并在任何地方都支持UTF-8.其它字符集用开源encodingcrate支持.

  • reader.read_exact(&mut buf)读取足够的数据以填充给定的缓冲区.参数类型是&[u8].如果读取器在读取buf.len()字节之前耗尽数据,则会返回ErrorKind :: UnexpectedEof错误.

这些是Readtrait的主要方法.此外,有四种适配器方法可以通过值接受reader,将其转换为迭代器或不同的读取器:

  • reader.bytes()返回一个在输入流的字节上的迭代器.项类型为io::Result<u8>,因此每个字节都需要进行错误检查.此外,每个字节调用一次reader.read(),如果读取器没有缓冲,这将是非常低效的.

  • reader.chars()相同,但迭代字符,将输入视为UTF-8.无效的UTF-8会导致InvalidData错误.

  • reader.chain(reader2)返回一个新的读取器,它生成所有来自reader的输入,然后是reader2的所有输入.

  • reader.take(n)返回一个新的读取器,它从与reader相同的源读取,但仅限于n个字节的输入.

没有关闭读取器的方法.读取器和写入器通常实现Drop,以便自动关闭它们.

缓冲读取器(Buffered Readers)

为了提高效率,读取器和写入器可以被 缓冲(buffered) ,这意味着它们有一块内存(缓冲区),可以在内存中保存一些输入或输出数据.这节省了系统调用,如图18-2所示.应用程序从BufReader读取数据,在示例中通过调用其.read_line()方法. BufReader反过来从操作系统获得更大的输入.

图18-2. 缓冲文件读取器.

这张图片不是按比例绘制的.BufReader的缓冲区的实际默认大小是几千字节,因此单个系统reader可以服务数百个.read_line()调用.这很重要,因为系统调用很慢.

(如图所示,操作系统也有一个缓冲区,原因相同:系统调用很慢,但从磁盘读取数据的速度更慢.)

缓冲读取器实现Read和第二个traitBufRead,它添加了以下方法:

  • reader.read_line(&mut line)读取一行文本并将其附加到line,即String.行末尾的换行符'\n'包含在line中.如果输入有Windows风格的行结尾"\r\n",则两个字符都包含在line中.

返回值是一个io::Result<usize>,读取的字节数,包括行结尾(如果有).

如果读取器位于输入的末尾,则保持line不变并返回Ok(0).

  • reader.lines()返回一个在输入行上的迭代器.项类型是io::Result<String>.换行符 不(not) 包含在字符串中.如果输入有Windows风格的行结尾"\r\n",则两个字符都被剥离.

这种方法几乎总是你想要用于文本输入的.接下来的两节展示了它的一些使用示例.

  • reader.read_until(stop_byte, &mut byte_vec)reader.split(stop _byte)类似于.read_line().lines(),但是面向字节,生成Vec<u8>而不是String.你选择分隔符stop_byte.

BufRead还提供了一对低级方法,.fill_buf().consume(n),用于直接访问读取器的内部缓冲区.有关这些方法的更多信息,请参阅在线文档.

接下来的两节将更详细地介绍缓冲读取器.

读取行(Reading Lines)

这是一个实现Unixgrep实用程序的函数.它搜索多行文本,通常是从另一个命令管道输入给定字符串:

  1. use std::io;
  2. use std::io::prelude::*;
  3. fn grep(target: &str) -> io::Result<()> {
  4. let stdin = io::stdin();
  5. for line_result in stdin.lock().lines() {
  6. let line = line_result?;
  7. if line.contains(target) {
  8. println!("{}", line);
  9. }
  10. }
  11. Ok(())
  12. }

由于我们想调用.lines(),我们需要一个实现BufRead的输入源.在这种情况下,我们调用io::stdin()来获取正在通过管道输送给我们的数据.但是,Rust标准库使用互斥锁(mutex)保护stdin.我们调用.lock()来锁定stdin以供当前线程独占使用;它返回一个实现BufReadStdinLock值.在循环结束时,StdinLock被删除,释放互斥锁.(如果没有互斥锁,两个线程试图同时从stdin读取将导致未定义行为.C具有相同的问题并以相同的方式解决它:所有C标准输入和输出函数在幕后获取锁定.唯一不同的是,在Rust中,锁是API的一部分.)

函数的其余部分很简单:它调用.lines()并在生成的迭代器上循环.因为这个迭代器产生Result值,我们使用?运算符检查错误.

假设我们希望将grep程序更进一步,并添加对在磁盘上搜索文件的支持.我们可以使这个函数泛型:

  1. fn grep<R>(target: &str, reader: R) -> io::Result<()>
  2. where R: BufRead
  3. {
  4. for line_result in reader.lines() {
  5. let line = line_result?;
  6. if line.contains(target) {
  7. println!("{}", line);
  8. }
  9. }
  10. Ok(())
  11. }

现在我们可以传递给它一个StdinLock或缓冲File:

  1. let stdin = io::stdin();
  2. grep(&target, stdin.lock())?; // ok
  3. let f = File::open(file)?;
  4. grep(&target, BufReader::new(f))?; // also ok

请注意,File不会自动缓冲.File实现了Read而不是BufRead.但是,为File或任何其他无缓冲读取器创建缓冲读取器很容易.BufReader::new(reader),这样做.(要设置缓冲区的大小,请使用BufReader::with_capacity(size, reader).)

在大多数语言中,默认情况下会缓冲文件.如果你想要无缓冲的输入或输出,你必须弄清楚如何关闭缓冲.在Rust中,FileBufReader是两个独立的库功能,因为有时你想要没有缓冲的文件,有时你想要没有文件的缓冲(例如,你可能想要缓冲来自网络的输入).

完整的程序,包括错误处理和一些粗略的参数解析,如下所示:

  1. // grep - Search stdin or some files for lines matching a given string.
  2. use std::error::Error;
  3. use std::io::{self, BufReader};
  4. use std::io::prelude::*;
  5. use std::fs::File;
  6. use std::path::PathBuf;
  7. fn grep<R>(target: &str, reader: R) -> io::Result<()>
  8. where R: BufRead
  9. {
  10. for line_result in reader.lines() {
  11. let line = line_result?;
  12. if line.contains(target) {
  13. println!("{}", line);
  14. }
  15. }
  16. Ok(())
  17. }
  18. fn grep_main() -> Result<(), Box<Error>> {
  19. // Get the command-line arguments. The first argument is the
  20. // string to search for; the rest are filenames.
  21. let mut args = std::env::args().skip(1);
  22. let target = match args.next() {
  23. Some(s) => s,
  24. None => Err("usage: grep PATTERN FILE...")?
  25. };
  26. let files: Vec<PathBuf> = args.map(PathBuf::from).collect();
  27. if files.is_empty() {
  28. let stdin = io::stdin();
  29. grep(&target, stdin.lock())?;
  30. } else {
  31. for file in files {
  32. let f = File::open(file)?;
  33. grep(&target, BufReader::new(f))?;
  34. }
  35. }
  36. Ok(())
  37. }
  38. fn main() {
  39. let result = grep_main();
  40. if let Err(err) = result {
  41. let _ = writeln!(io::stderr(), "{}", err);
  42. }
  43. }

收集行(Collecting Lines)

一些读取器方法(包括.lines())返回生成Result值的迭代器.当你第一次想要将文件的所有行收集到一个大向量中时,你将遇到一个摆脱Result的问题.

  1. // ok, but not what you want
  2. let results: Vec<io::Result<String>> = reader.lines().collect();
  3. // error: can't convert collection of Results to Vec<String>
  4. let lines: Vec<String> = reader.lines().collect();

第二次尝试不编译:错误会发生什么?直接的解决方案是编写for循环并检查每个项目是否有错误:

  1. let mut lines = vec![];
  2. for line_result in reader.lines() {
  3. lines.push(line_result?);
  4. }

不错;但是在这里使用.collect()会很好,事实证明我们可以.我们只需要知道要求的类型:

  1. let lines = reader.lines().collect::<io::Result<Vec<String>>>()?;

这个如何工作的?标准库包含FromIterator的用于Result的实现—很容易在在线文档中忽略—这使其成为可能:

  1. impl <T, E, C> FromIterator<Result<T, E>> for Result<C, E>
  2. where C: FromIterator<T>
  3. {
  4. ...
  5. }

这就是说:如果你可以将类型为T的项收集到类型C(“where C: FromIterator<T>“)的集合中,那么你可以将Result<T, E>类型的项收集到Result<C, E>(“FromIterator<Result<T, E>> for Result<C, E>“)类型的结果中.

换句话说,io::Result<Vec<String>>是一个集合类型,因此.collect()方法可以创建和填充该类型的值.

写入器(Writers)

正如我们所见,输入主要是使用方法完成的.输出有点不同.

在整本书中,我们使用println!()来生成纯文本输出.

  1. println!("Hello, world!");
  2. println!("The greatest common divisor of {:?} is {}",
  3. numbers, d);

还有一个print!()宏,它不会在末尾添加换行符.print!()println!()的格式化代码与format!宏相同,在第413页的”格式化值(Formatting Values)”中进行了介绍.

要将输出发送到写入器,请使用write!()writeln!()宏.它们与print!()println!()相同,除了两个不同之外:

  1. writeln!(io::stderr(), "error: world not helloable")?;
  2. writeln!(&mut byte_vec, "The greatest common divisor of {:?} is {}",
  3. numbers, d)?;

一个区别是write宏每个都接受一个额外的第一个参数,一个编入器.另一个是它们返回Result,因此必须处理错误.这就是我们每行末尾使用?运算符的原因.

print宏不返回Result;如果写入失败,它们只会恐慌.因为他们写入到终端,这种情况很少见.

Writetrait有以下方法:

  • writer.write(&buf)将切片buf中的一些字节写入底层流.它返回一个io::Result<usize>.成功时,这会给出写入的字节数,可能小于buf.len(),在流的一时兴起时.

类似于Reader::read(),这是一个低级方法,你应该避免直接使用.

  • writer.write_all(&buf)将切片buf中的所有字节写入.返回Result<()>.

  • writer.flush()将任何缓冲的数据刷新到底层流.返回Result<()>.

与读取器一样,写入器在被删除时会自动关闭.

正如BufReader::new(reader)为任何读取器添加缓冲区一样,BufWriter::new(writer)为任何写入器添加缓冲区.

  1. let file = File::create("tmp.txt")?;
  2. let writer = BufWriter::new(file);

要设置缓冲区的大小,请使用BufWriter::with_capacity(size,writer).

删除BufWriter后,所有剩余的缓冲数据都将写入底层写入器.但是,如果在写入期间发生错误,则该错误被 忽略(ignored) .(因为这发生在BufWriter.drop()方法中,所以没有用来报告错误.)为了确保你的应用程序注意到所有输出错误,请在删除它们之前手动.flush()缓冲写入器.

文件(Files)

我们已经见过了两种打开文件的方法:

  • File::open(filename)打开现有文件以供读取.它返回一个io::Result<File>,如果该文件不存在则是个错误.

  • File::create(filename)创建一个用于写入的新文件.如果存在具有给定文件名的文件,则会将其截断.

请注意,File类型位于文件系统模块中,std::fs,而不是std::io.

如果这些都不符合要求,你可以使用OpenOptions指定所需的确切行为:

  1. use std::fs::OpenOptions;
  2. let log = OpenOptions::new()
  3. .append(true) // if file exists, add to the end
  4. .open("server.log")?;
  5. let file = OpenOptions::new()
  6. .write(true)
  7. .create_new(true) // fail if file exists
  8. .open("new_file.txt")?;

方法.append(),.write(),.create_new()等设计为链式:每个返回self.这种方法链式设计模式很常见,在Rust中有一个名称:它被称为 构建器(builder) .std::process::Command是另一个例子.有关OpenOptions的更多详细信息,请参阅在线文档.

File打开后,其行为与任何其他读取器或写入器一样.如果需要,你可以添加缓冲区.删除时,File将自动关闭.

探针(Seeking)

File还实现了Seektrait,这意味着你可以在File中跳转,而不是从头到尾一次读取或写入.Seek的定义如下:

  1. pub trait Seek {
  2. fn seek(&mut self, pos: SeekFrom) -> io::Result<u64>;
  3. }
  4. pub enum SeekFrom {
  5. Start(u64),
  6. End(i64),
  7. Current(i64)
  8. }

由于枚举,seek方法很有表现力:使用file.seek(SeekFrom::Start(0))倒回到开头,file.seek(SeekFrom::Current(-8))返回几个字节,等等.

在文件中探寻是很慢的.无论你使用的是硬盘还是固态硬盘(SSD),探寻所需的时间都相当于读取几兆字节(megabytes)的数据.

其它的读取器和写入器类型(Other Reader and Writer Types)

在本章的前面,我们给出了一些除File以外的实现ReadWrite的类型的例子.在这里,我们将提供有关这些类型的更多详细信息.

  • io::stdin()返回标准输入流的读取器.它的类型是io::Stdin.由于这是由所有线程共享的,因此每次读取都会获取并释放互斥锁.

Stdin有一个.lock()方法,它获取互斥锁并返回一个io::StdinLock,一个缓冲读取器,它持有互斥锁直到它被删除.因此,StdinLock上的单独操作可以避免互斥锁开销.我们在第436页的”读取行(Reading Lines)”中使用此方法显示了示例代码.

由于技术原因,io::stdin().lock()不起作用.该锁保存对Stdin值的引用,这意味着Stdin值必须存储在某处,以便它能够存活足够长的时间:

  1. let stdin = io::stdin();
  2. let lines = stdin.lock().lines(); // ok
  • io::stdout()io::stderr()返回标准输出和标准错误流的写入器.这些也有互斥锁和.lock()方法.
  • Vec<u8>实现Write.写入Vec<u8>会使用新数据扩展向量.

(但是,String 不(not) 实现Write.要使用Write构建字符串,首先写入Vec<u8>,然后使用String::from_utf8(vec)将向量转换为字符串.)

  • Cursor::new(buf)创建一个Cursor,一个从buf读取的缓冲读取器.这是你创建从String读取的读取器的方法.参数buf可以是任何实现AsRef<[u8]>的类型,因此你也可以传递&[u8],&strVec<u8>.

Cursor在内部是微不足道的.他们只有两个字段:buf本身;和一个整数,buf中的偏移量,下一次读取将在那儿开始.初识位置为0.

游标实现Read,BufReadSeek.如果buf的类型是&mut [u8]Vec<u8>,那么Cursor也会实现Write.写入游标会覆盖从当前位置开始的buf中的字节.如果你试着写超过一个&mut [u8]的结尾,你会得到一个部分写或一个io::Error.不过,使用游标讲Vec<u8>的末尾写过去是可以的,它会增长向量.因此,Cursor<&mut [u8]>Cursor<Vec<u8>>因此实现了所有四个std::io::preludetrait.

  • std::net::TcpStream表示TCP网络连接.由于TCP支持双向通信,因此它既是读取器又是写入器.

静态方法TcpStream::connect(("hostname",PORT))尝试连接到服务器并返回io::Result<TcpStream>.

  • std::process::Command支持生成子进程并将数据传输到其标准输入,如下所示:
  1. use std::process::{Command, Stdio};
  2. let mut child =
  3. Command::new("grep")
  4. .arg("-e")
  5. .arg("a.*e.*i.*o.*u")
  6. .stdin(Stdio::piped())
  7. .spawn()?;
  8. let mut to_child = child.stdin.take().unwrap();
  9. for word in my_words {
  10. writeln!(to_child, "{}", word)?;
  11. }
  12. drop(to_child); // close grep's stdin, so it will exit
  13. child.wait()?;

child.stdin的类型是Option<std::process::ChildStdin>;这里我们在设置子进程时使用了.stdin(Stdio::piped()),所以当.spawn()成功时,肯定会填充child.stdin.如果我们没有成功,child.stdin将是None.

Command也有类似的方法.stdout().stderr(),可用于请求child.stdoutchild.stderr中的读取器.

std::io模块还提供了一些函数,可以返回琐碎的读取器和写入器.

  • io::sink()是无操作(no-op)写入器.所有的write方法都返回Ok,但数据才被丢弃.

  • io::empty()是无操作(no-op)读取器.读取总是成功,但返回输入结束(end-of-input).

  • io::repeat(byte)返回一个无限重复给定字节的读取器.

二进制数据,压缩和序列化(Binary Data, Compression, and Serialization)

许多开源crate都在std::io框架上构建,以提供额外的功能.

byteordercrate提供了ReadBytesExtWriteBytesExttrait,这些traits为所有读取器和写入器添加了二进制输入和输出的方法:

  1. use byteorder::{ReadBytesExt, WriteBytesExt, LittleEndian};
  2. let n = reader.read_u32::<LittleEndian>()?;
  3. writer.write_i64::<LittleEndian>(n asi64)?;

flate2crate提供了读取和写入gzip的数据的适配器方法:

  1. use flate2::FlateReadExt;
  2. let file = File::open("access.log.gz")?;
  3. letmut gzip_reader = file.gz_decode()?;

serdecrate用于序列化和反序列化:它在Rust结构和字节之间来回转换.我们之前曾在第247页的”traits和其他人的类型(Traits and Other People’s Types)”中提到过这一点.现在我们可以仔细看看.

假设我们有一些数据—文本冒险游戏的地图—存储在HashMap中:

  1. type RoomId = String; // each room has a unique name
  2. type RoomExits = Vec<(char, RoomId)>; // ...and a list of exits
  3. type RoomMap = HashMap<RoomId, RoomExits>; // room names and exits, simple
  4. // Create a simple map.
  5. let mut map = RoomMap::new();
  6. map.insert("Cobble Crawl".to_string(),
  7. vec![('W', "Debris Room".to_string())]);
  8. map.insert("Debris Room".to_string(),
  9. vec![('E', "Cobble Crawl".to_string()),
  10. ('W', "Sloping Canyon".to_string())]);
  11. ...

将此数据转换为JSON以进行输出只需几行代码:

  1. use std::io;
  2. use serde::Serialize;
  3. use serde_json::Serializer;
  4. let mut serializer = Serializer::new(io::stdout());
  5. map.serialize(&mut serializer)?

此代码使用serde::Serializetrait的serialize方法.该库将这个trait附加到它知道如何序列化的所有类型,并且包括我们数据中出现的所有类型:字符串,字符,元组,向量和HashMap.

serde很灵活.在这个程序中,输出是JSON数据,因为我们选择了serde_json序列化器.其他格式,如MessagePack,也可用.同样,你可以将此输出发送到文件,Vec<u8>或任何其他写入器.上面的代码在stdout上打印数据.这里是:

  1. {"Debris Room":[["E","Cobble Crawl"],["W","Sloping Canyon"]],"Cobble Crawl":
  2. [["W","Debris Room"]]}

serde还包括对派生两个关键serdetrait的支持:

  1. #[derive(Serialize, Deserialize)]
  2. struct Player {
  3. location: String,
  4. items: Vec<String>,
  5. health: u32
  6. }

从Rust 1.17开始,这个#[derive]属性在设置项目时需要一些额外的步骤.我们不会在这里讨论;有关详细信息,请参阅serde文档.简而言之,构建系统为 Player自动生成serde::Serializeserde::Deserialize的实现,因此序列化Player值很简单:

  1. player.serialize(&mut serializer)?;

输出如下:

  1. {"location":"Cobble Crawl","items":["a wand"],"health":3}

文件和目录(Files and Directories)

接下来的几节将介绍Rust用于处理文件和目录的功能,这些功能位于std::pathstd::fs模块中.所有这些功能都涉及使用文件名,因此我们将从文件名类型开始.

OsStr和Path(OsStr and Path)

不方便的是,你的操作系统不会强制文件名是有效的Unicode.以下是两个用于创建文本文件的Linux shell命令.只有第一个使用有效的UTF-8文件名.

  1. $ echo "hello world" > ô.txt
  2. $ echo "O brave new world, that has such filenames in't" > $'\xf4'.txt
  3. $

两个命令都不加注释地传递,因为Linux内核不知道来自Ogg Vorbis的UTF-8.对于内核,任何字节串(不包括空字节和斜杠)都是可接受的文件名.在Windows上的也有类似情况:几乎任何16位的”宽字符”字符串都是可接受的文件名,甚至是无效的UTF-16字符串.操作系统处理的其他字符串也是如此,比如命令行参数和环境变量.

Rust字符串始终是有效的Unicode.文件名在实践中 几乎(almost) 总是Unicode,但Rust必须以某种方式应对罕见的情况.这就是Rust有std::ffi::OsStrOsString的原因.

OsStr是一种字符串类型,它是UTF-8的超集.它的工作是能够代表当前系统上的所有文件名,命令行参数和环境变量, 无论它们是否是有效的Unicode(whether they’re valid Unicode or not) .在Unix上,OsStr可以保存任何字节序列.在Windows上,使用UTF-8扩展存储OsStr,它可以编码任何16位值序列,包括不匹配的代理.

所以我们有两种字符串类型:str用于实际的Unicode字符串;和OsStr,无论你的操作系统什么胡说都可以.我们将再介绍一个:std::path::Path,用于文件名.这个纯粹是为了方便.PathOsStr完全相同,但它添加了许多方便的文件名相关的方法,我们将在下一节中介绍.对绝对路径和相对路径都使用Path.对于路径的单个组件,请使用OsStr.

最后,对于每种字符串类型,都有一个相应的 拥有(owning) 类型:String拥有堆分配的str,std::ffi::OsString拥有堆分配的OsStr,std::path::PathBuf拥有堆分配Path.

str OsStr Path
无符号类型,总是通过引用传递
可以包含任何Unicode文本
看起来就像UTF-8一样
可以包含非Unicode数据
文本处理方法
文件名相关方法
拥有的,可增长的,堆分配的等价物 String OsString PathBuf
转换为拥有的类型 .to_string() .to_os_string() .to_path_buf()

所有这三种类型都实现了一个共同trait,AsRef<Path>,因此我们可以很容易地声明一个接受”任何文件名类型(any filename type)”作为参数的泛型函数.这使用了我们在第294页的”AsRef和AsMut(AsRef and AsMut)”中展示的技术:

  1. use std::path::Path;
  2. use std::io;
  3. fn swizzle_file<P>(path_arg: P) -> io::Result<()>
  4. where P: AsRef<Path>
  5. {
  6. let path = path_arg.as_ref();
  7. ...
  8. }

接受path参数的所有标准函数和方法都使用此技术,因此你可以自由地将字符串字面量传递给它们中的任何一个.

Path和PathBuf方法(Path and PathBuf Methods)

Path提供以下方法,其中包括:

  • Path::new(str)&str&OsStr转换为&Path.这不会复制字符串:新的&Path指向与原始&str&OsStr相同的字节.
  1. use std::path::Path;
  2. let home_dir = Path::new("/home/fwolfe");

(类似的方法OsStr::new(str)&str转换为&OsStr.)

  • path.parent()返回路径的父目录(如果有).返回类型是Option<&Path>.

这不会复制路径:path的父目录始终是path的子字符串.

  1. assert_eq!(Path::new("/home/fwolfe/program.txt").parent(),
  2. Some(Path::new("/home/fwolfe")));
  • path.file_name()返回路径的最后一个组件(如果有).返回类型是Option<&OsStr>.

在典型的情况下,path由一个目录组成,然后是斜杠,然后是文件名,这将返回文件名.

  1. assert_eq!(Path::new("/home/fwolfe/program.txt").file_name(),
  2. 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.

  1. let path1 = Path::new("/usr/share/dict");
  2. assert_eq!(path1.join("words"),
  3. Path::new("/usr/share/dict/words"));

如果path2是绝对路径,则只返回path2的副本,因此可以使用此方法将任何路径转换为绝对路径:

  1. let abs_path = std::env::current_dir()?.join(any_path);
  • path.components()返回给定路径的组件上的从左到右的迭代器.这个迭代器的项类型是std::path::Component,这个枚举可以表示文件名中可以出现的所有不同部分:
  1. pub enumComponent<'a> {
  2. Prefix(PrefixComponent<'a>), // Windows-only: a drive letter or share
  3. RootDir, // the root directory, `/` or `\`
  4. CurDir, // the `.` special directory
  5. ParentDir, // the `..` special directory
  6. Normal(&'a OsStr) // plain file and directory names
  7. }

例如,Windows路径 \venice\Music\A Love Supreme\04-Psalm.mp3 包含一个代表 \venice\MusicPrefix,后跟一个RootDir,然后是两个代表 A Love Supreme04-Psalm.mp3Normal组件.

有关详细信息,请参阅在线文档.

这些方法适用于内存中的字符串.Path还有一些查询文件系统的方法:.exists(),.is_ file(),.is_dir(),.read_dir(),.canonicalize()等.有关详细信息,请参阅在线文档。

Path转换为字符串有三种方法.每个都允许在Path中可能存在无效的UTF-8.

  • path.to_str()Path转换为字符串,作为Option<&str>.如果path无效UTF-8,则返回None.
  1. if let Some(file_str) = path.to_str() {
  2. println!("{}", file_str);
  3. } // ...otherwise skip this weirdly named file
  • path.to_string_lossy()基本上是相同的东西.但它设法在所有情况下返回某种字符串.如果path不是有效的UTF-8,则这些方法会制作副本,用Unicode替换字符U+FFFD(‘�’)替换每个无效字节序列.

返回类型是std::borrow::Cow<str>:一个借用或拥有的字符串.要从此值获取String,请使用其.to_owned()方法.(有关Cow的更多信息,请参阅第300页的”工作中的Borrow和ToOwned:谦卑的Cow(Borrow and ToOwned at Work: The Humble Cow)”.)

  • path.display()用于打印路径:
  1. println!("Download found。you put it in:{}", dir_path.display());

返回的值不是字符串,但它实现了std::fmt::Display,因此它可以与format!(),println!()和同类一起使用.如果路径不是有效的UTF-8,则输出可能包含�字符.

文件系统访问函数(Filesystem Access Functions)

表18-1显示了std::fs中的一些函数及其在Unix和Windows上的近似等价物.所有这些函数都返回io::Result值.除非另有说明,否则它们是Result<()>.

表18-1. 文件系统访问函数摘要.

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<u64>
rename(src_path, dest_path)
hard_link(src_path, dest_path)
类似cp -p
rename()
link()
CopyFileEx()
MoveFileEx()
CreateHardLink()
检查 canonicalize(path) -> Result<PathBuf>
metadata(path) -> Result<Metadata>
symlink_metadata(path) -> Result<Metadata>
read_dir(path) -> Result<ReadDir>
read_link(path) -> Result<PathBuf>
realpath()
stat()
lstat()
opendir()
readlink()
GetFinalPathNameByHandle()
GetFileInformationByHandle()
GetFileInformationByHandle()
FindFirstFile()
FSCTL_GET_REPARSE_POINT
权限 set_permissions(path, perm) chmod() SetFileAttributes()

(copy()返回的数字是复制文件的大小(以字节为单位).有关创建符号链接的信息,请参阅第451页的”特定于平台的功能(Platform-Specific Features)”.)

正如你所看到的,Rust致力于提供在Windows以及macOS,Linux和其他Unix系统上可预测的可移植的函数.

有关文件系统的完整教程超出了本书的范围,但如果你对这些函数中的任何一个感到好奇,你可以在线轻松找到有关它们的更多信息.我们将在下一节中展示一些示例.

所有这些函数都是通过调用操作系统来实现的.例如,std::fs::canonicalize(path)不仅仅使用字符串处理来消除来自给定的path....它使用当前工作目录解析相对路径,并追踪符号链接.如果路径不存在则会出错.

std::fs::metadata(path)std::fs::symlink_metadata(path)生成的Metadata类型包含诸如文件类型和大小,权限和时间戳之类的信息.与往常一样,请查阅文档以获取详细信息.

为方便起见,Path类型中有一些作为方法内置:例如path.metadata(),与std::fs::metadata(path)相同.

读取目录(Reading Directories)

要列出目录的内容,请使用std::fs::read_dir,或等效地,使用Path.read_dir()方法:

  1. for entry_result in path.read_dir()? {
  2. let entry = entry_result?;
  3. println!("{}", entry.file_name().to_string_lossy());
  4. }

注意在这段代码中两个?的使用.第一行检查打开目录的错误.第二行检查读取下一个条目的错误.

entry的类型是std::fs::DirEntry,它是一个只有几个方法的结构:

  • entry.file_name()是文件或目录的名称,作为OsString.

  • 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()获取有关此条目的其余元数据.

特殊目录....在读取目录时未列出.

这是一个更实际的例子.以下代码以递归方式将目录树从一个位置复制到磁盘上的另一个位置:

  1. use std::fs;
  2. use std::io;
  3. use std::path::Path;
  4. /// Copy the existing directory `src` to the target path `dst`.
  5. fn copy_dir_to(src: &Path, dst: &Path) -> io::Result<()> {
  6. if !dst.is_dir() {
  7. fs::create_dir(dst)?;
  8. }
  9. for entry_result in src.read_dir()? {
  10. let entry = entry_result?;
  11. let file_type = entry.file_type()?;
  12. copy_to(&entry.path(), &file_type, &dst.join(entry.file_name()))?;
  13. }
  14. Ok(())
  15. }

一个单独的函数copy_to复制单个目录条目:

  1. /// Copy whatever is at `src` to the target path `dst`.
  2. fn copy_to(src: &Path, src_type: &fs::FileType, dst: &Path) -> io::Result<()> {
  3. if src_type.is_file() {
  4. fs::copy(src, dst)?;
  5. } else if src_type.is_dir() {
  6. copy_dir_to(src, dst)?;
  7. } else {
  8. return Err(io::Error::new(io::ErrorKind::Other,
  9. format!("don't know how to copy: {}",
  10. src.display())));
  11. }
  12. Ok(())
  13. }

特定于平台的功能(Platform-Specific Features)

到目前为止,我们的copy_to函数可以复制文件和目录.假设我们也想在Unix上支持符号链接.

没有可移植的方法来创建适用于Unix和Windows的符号链接,但标准库提供了特定于Unix的symlink函数,

  1. use std::os::unix::fs::symlink;

有了这个,我们的工作很容易.我们只需要在copy_to中的if表达式中添加一个分支:

  1. ...
  2. } else if src_type.is_symlink() {
  3. let target = src.read_link()?;
  4. symlink(target, dst)?;
  5. ...

只要我们为Unix系统编译程序,例如Linux和macOS,这就可以工作.

std::os模块包含各种特定于平台的功能,如symlink.标准库中std::os的实际主体看起来像这样(有一些诗意的许可证):

  1. //! OS-specific functionality.
  2. #[cfg(unix)] pub mod unix;
  3. #[cfg(windows)] pub mod windows;
  4. #[cfg(target_os = "ios")] pub mod ios;
  5. #[cfg(target_os = "linux")] pub mod linux;
  6. #[cfg(target_os = "macos")] pub mod macos;
  7. ...

#[cfg]属性表示条件编译:每个模块仅存在于某些平台上.这就是为什么我们修改的程序,使用std::os::unix,只能为Unix成功编译:在其他平台上,std::os::unix不存在.

如果我们希望我们的代码在所有平台上编译,并且在Unix上支持符号链接,我们也必须在我们的程序中使用#[cfg].在这种情况下,最简单的方法是在Unix上导入symlink,同时在其他系统上定义我们自己的symlink存根:

  1. #[cfg(unix)]
  2. use std::os::unix::fs::symlink;
  3. /// Stub implementation of `symlink` for platforms that don't provide it.
  4. #[cfg(not(unix))]
  5. fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, _dst: Q)
  6. -> std::io::Result<()>
  7. {
  8. Err(io::Error::new(io::ErrorKind::Other,
  9. format!("can't copy symbolic link: {}",
  10. src.as_ref().display())))
  11. }

在撰写本文时,https://doc.rust-lang.org/std上的在线文档是通过 在Linux上(on Linux) 运行标准库上的rustdoc生成的.这意味着macOS,Windows和其他平台的系统特定功能未显示在在线文档中.找到它的最佳方法是使用rustup doc查看适用于你平台的HTML文档.当然,另一种选择是查阅源码,它可以在线获得.

事实证明,symlink是一种特殊情况.大多数特定于Unix的功能不是独立函数,而是为标准库类型添加新方法的扩展trait.(我们在第247页的”Trait和其他人的类型”中介绍了扩展trait.)有一个prelude模块可用于同时启用所有这些扩展:

  1. use std::os::unix::prelude::*;

例如,在Unix上,这会将一个.mode()方法添加到std::fs::Permissions,从而提供对代表Unix权限的底层u32值的访问.类似地,它使用对于底层struct stat值的字段—例如.uid()(即文件所有者的用户ID)的访问器扩展std::fs::Metadata.

总而言之,std::os中的内容非常基本.通过第三方crate可以获得更多特定于平台的功能,例如用于访问Windows注册表的winreg.

网络(NetWorking)

关于网络的教程远远超出了本书的范围.但是,如果你已经对网络编程有所了解,本节将帮助你开始使用Rust中的网络.

对于低级网络代码,请从std::net模块开始,该模块为TCP和UDP网络提供跨平台支持.使用native_tlscrate进行SSL/TLS支持.

这些模块提供了在网络上直接的,阻塞式输入和输出的构建块.你可以使用std::net在几行代码中编写一个简单的服务器,并为每个连接生成一个线程.例如,这是一个”echo”服务器:

  1. use std::net::TcpListener;
  2. use std::io;
  3. use std::thread::spawn;
  4. /// Accept connections forever, spawning a thread for each one.
  5. fn echo_main(addr: &str) -> io::Result<()> {
  6. let listener = TcpListener::bind(addr)?;
  7. println!("listening on {}", addr);
  8. loop {
  9. // Wait for a client to connect.
  10. let (mut stream, addr) = listener.accept()?;
  11. println!("connection received from {}", addr);
  12. // Spawn a thread to handle this client.
  13. let mut write_stream = stream.try_clone()?;
  14. spawn(move || {
  15. // Echo everything we receive from `stream` back to it.
  16. io::copy(&mut stream, &mut write_stream)
  17. .expect("error in client thread: ");
  18. println!("connection closed");
  19. });
  20. }
  21. }
  22. fn main() {
  23. echo_main("127.0.0.1:17007").expect("error: ");
  24. }

echo服务器只是重复发送给它的所有内容.这种代码与您在Java或Python中编写的代码没有太大区别.(我们将在下一章中介绍std::thread::spawn().)

但是,对于高性能服务器,你需要使用异步(asynchronous)输入和输出.miocrate提供所需的支持.MIO非常低级.它提供了一个简单的事件循环(event loop)和用于读取,写入,连接和接受连接的异步方法—基本上是整个网络API的异步副本.每当异步操作完成时,MIO都会将事件传递给你编写的事件处理程序方法.

还有一个实验性的tokiocrate,它将mio事件循环包装在futures-based API中,让人联想到JavaScript的promises.

第三方crate支持更高​​级的协议.例如,reqwest crate为HTTP客户端提供了一个漂亮的API.这是一个完整的命令行程序,它使用http:https:URL获取任何文档并将其转储到你的终端.此代码使用reqwest = "0.5.1"编写.

  1. extern crate reqwest;
  2. use std::error::Error;
  3. use std::io::{self, Write};
  4. fn http_get_main(url: &str) -> Result<(), Box<Error>> {
  5. // Send the HTTP request and get a response.
  6. let mut response = reqwest::get(url)?;
  7. if !response.status().is_success() {
  8. Err(format!("{}", response.status()))?;
  9. }
  10. // Read the response body and write it to stdout.
  11. let stdout = io::stdout();
  12. io::copy(&mut response, &mut stdout.lock())?;
  13. Ok(())
  14. }
  15. fn main() {
  16. let args: Vec<String> = std::env::args().collect();
  17. if args.len() != 2 {
  18. writeln!(io::stderr(), "usage: http-get URL").unwrap();
  19. return;
  20. }
  21. if let Err(err) = http_get_main(&args[1]) {
  22. writeln!(io::stderr(), "error: {}", err).unwrap();
  23. }
  24. }

HTTP服务器的iron框架提供了一些高级的功能,比如BeforeMiddlewareAfterMiddlewaretrait,它们可以帮助你从可拔插部件编写应用程序.websocketcrate实现了WebSocket协议.等等.Rust是一种拥有繁忙的开源生态系统的年轻语言.对网络的支持正在迅速扩大.