Appearance
26.3 I/O
标准库中也提供了一系列 I/O 相关的功能。虽然功能比较基础,但好在是跨平台的。如果用户需要更丰富的功能,可以去寻求外部的开源库。
26.3.1 平台相关字符串
要跟操作系统打交道,首先需要介绍的是两个字符串类型:OsString
以及它所对应的字符串切片类型OsStr
。它们存在于std::ffi
模块中。
Rust 标准的字符串类型是 String 和 str。它们的一个重要特点是保证了内部编码是统一的 utf-8。 但是,当我们和具体的操作系统打交道时,统一的 utf-8 编码是不够用的,某些操作系统并没有规定一定是用的 utf-8 编码。 所以,在和操作系统打交道的时候,String/str 类型并不是一个很好的选择。比如在 Windows 系统上,字符一般是用 16 位数字来表示的。
为了应付这样的情况,Rust 在标准库中又设计了 OsString/OsStr 来处理这样的情况。这两种类型携带的方法跟 String/str 非常类似,用起来几乎没什么区别,它们之间也可以相互转换。
举个需要用到 OsStr 场景的例子:
rust
use std::path::PathBuf;
fn main() {
let mut buf = PathBuf::from("/");
buf.set_file_name("bar");
if let Some(s) = buf.to_str() {
println!("{}", s);
} else {
println!("invalid path");
}
}
上面这个例子是处理操作系统中的路径,就必须用 OsString/OsStr 这两个类型。PathBuf 的set_file_name
方法的签名是这样的:
rust
fn set_file_name<S: AsRef<OsStr>>(&mut self, file_name: S)
它要求,第二个参数必须满足AsRef<OsStr>
的约束。而查看 str 类型的文档,我们可以看到:
rust
impl AsRef<OsStr> for str
所以,&str 类型可以直接作为参数在这个方法中使用。
另外,当我们想把&PathBuf
转为&str
类型的时候,使用了to_str
方法,返回的是一个Option<&str>
类型。这是为了错误处理。因为 PathBuf 内部是用 OsString 存储的字符串,它未必能成功转为 utf-8 编码。而想要把&PathBuf
转为&OsStr
则简单多了,这种转换不需要错误处理,因为它们是同样的编码。
26.3.2 文件和路径
Rust 标准库中用 PathBuf 和 Path 两个类型来处理路径。它们之间的关系就类似 String 和 str 之间的关系:一个对内部数据有所有权,还有一个只是借用。实际上,读源码可知,PathBuf 里面存的是一个 OsString,Path 里面存的是一个 OsStr。这两个类型定义在std::path
模块中。
Rust 对文件操作主要是通过std::fs::File
来完成的。这个类型定义了一些成员方法,可以实现打开、创建、复制、修改权限等文件操作。std::fs 模块下还有一些独立函数,比如remove_file
、soft_link
等,也是非常有用的。
对文件的读写,则需要用到std::io
模块了。这个模块内部定义了几个重要的 trait,比如 Read/Write。File 类型也实现了 Read 和 Write 两个 trait,因此它拥有一系列方便读写文件的方法,比如 read、read_to_end、read_to_string 等。这个模块还定义了 BufReader 等类型。我们可以把任何一个满足 Read trait 的类型再用 BufReader 包一下,实现有缓冲的读取。
下面用一个示例来演示说明这些类型的使用方法:
rust
use std::io::prelude::*;
use std::io::BufReader;
use std::fs::File;
fn test_read_file() -> Result<(), std::io::Error> {
let mut path = std::env::home_dir().unwrap();
path.push(".rustup");
path.push("settings");
path.set_extension("toml");
let file = File::open(&path)?;
let reader = BufReader::new(file);
for line in reader.lines() {
println!("Read a line: {}", line?);
}
Ok(())
}
fn main() {
match test_read_file() {
Ok(_) => {}
Err(e) => {
println!("Error occured: {}", e);
}
}
}
26.3.3 标准输入输出
前面我们已经多次使用了 println!宏输出一些信息。这个宏很方便,特别适合在小程序中随手使用。但是如果你需要对标准输入输出作更精细的控制,则需要使用更复杂一点的办法。
在 C++里面,标准输入输出流 cin、cout 是全局变量。在 Rust 中,基于线程安全的考虑,获取标准输入输出的实例需要调用函数,分别为std::io::stdin()
和std::io::stdout()
。stdin()
函数返回的类型是 Stdin 结构体。这个结构体本身已经实现了 Read trait,所以,可以直接在其上调用各种读取方法。但是这样做效率比较低,因为为了线程安全考虑,每次读取的时候,它的内部都需要上锁。提高执行效率的办法是手动调用lock()
方法,在这个锁的期间内多次调用读取操作,来避免多次上锁。
示例如下:
rust
use std::io::prelude::*;
use std::io::BufReader;
fn test_stdin() -> Result<(), std::io::Error> {
let stdin = std::io::stdin();
let handle = stdin.lock();
let reader = BufReader::new(handle);
for line in reader.lines() {
let line = line?;
if line.is_empty() {
return Ok(());
}
println!("Read a line: {}", line);
}
Ok(())
}
fn main() {
match test_stdin() {
Ok(_) => {}
Err(e) => {
println!("Error occured: {}", e);
}
}
}
26.3.4 进程启动参数
大家应该注意到了,Rust 的 main
函数的签名和 C/C++ 不一致。在 C/C++ 里面,一般进程启动参数是直接用指针传递给 main
函数的,进程返回值是通过 main
函数的返回值来决定的。
在 Rust 中,进程启动参数是调用独立的函数std::env::args()
来得到的,或者使用std::env::args_os()
来得到,具体查看这里,进程返回值也是调用独立函数std::process::exit()
来指定的。示例如下:
rust
fn main() {
if std::env::args().any(|arg| arg == "-kill") {
std::process::exit(1);
}
for arg in std::env::args() {
println!("{}", arg);
}
}
同样,标准库只提供最基本的功能。如果读者需要功能更强大、更容易使用的命令行参数解析器,可以到 crates.io 上搜索相关开源库,clap 或者 getopts 都是很好的选择。