Skip to content

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_filesoft_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 都是很好的选择。

Released under the MIT License