使用文件

现在我们在电脑上使用Rust,我们可以开始处理文件了。你会注意到,现在我们会开始在代码中越来越多的看到Result。这是因为一旦你开始处理文件和类似的事情,很多事情都会出错。一个文件可能不在那里,或者计算机无法读取它。

你可能还记得,如果你想使用?运算符,调用它的函数必须返回一个Result。如果你记不住错误类型,你可以什么都不给它,让编译器告诉你。让我们用一个试图用.parse()创建一个数字的函数来试试。

// ⚠️
fn give_number(input: &str) -> Result<i32, ()> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88"));
    println!("{:?}", give_number("5"));
}

编译器告诉我们到底该怎么做。

error[E0308]: mismatched types
 --> src\main.rs:4:5
  |
3 | fn give_number(input: &str) -> Result<i32, ()> {
  |                                --------------- expected `std::result::Result<i32, ()>` because of return type
4 |     input.parse::<i32>()
  |     ^^^^^^^^^^^^^^^^^^^^ expected `()`, found struct `std::num::ParseIntError`
  |
  = note: expected enum `std::result::Result<_, ()>`
             found enum `std::result::Result<_, std::num::ParseIntError>`

很好! 所以我们只要把返回值改成编译器说的就可以了:

use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88"));
    println!("{:?}", give_number("5"));
}

现在程序可以运行了!

Ok(88)
Ok(5)

所以现在我们想用?,如果能用就直接给我们数值,如果不能用就给错误。但是如何在fn main()中做到这一点呢?如果我们尝试在main中使用?,那就不行了。

// ⚠️
use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() {
    println!("{:?}", give_number("88")?);
    println!("{:?}", give_number("5")?);
}

它说:

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
  --> src\main.rs:8:22
   |
7  | / fn main() {
8  | |     println!("{:?}", give_number("88")?);
   | |                      ^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
9  | |     println!("{:?}", give_number("5")?);
10 | | }
   | |_- this function should return `Result` or `Option` to accept `?`

但实际上main()可以返回一个Result,就像其他函数一样。如果我们的函数能工作,我们不想返回任何东西(main()并没有给其他任何东西)。而如果它不工作,我们将错误返回。所以我们可以这样写:

use std::num::ParseIntError;

fn give_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() -> Result<(), ParseIntError> {
    println!("{:?}", give_number("88")?);
    println!("{:?}", give_number("5")?);
    Ok(())
}

不要忘了最后的Ok(()):这在Rust中是很常见的,它的意思是Ok,里面是(),也就是我们的返回值。现在它打印出来了:

88
5

只用.parse()的时候不是很有用,但是用文件就很有用。这是因为?也为我们改变了错误类型。下面是用简单英语写的?运算符页面:

If you get an `Err`, it will get the inner error. Then `?` does a conversion using `From`. With that it can change specialized errors to more general ones. The error it gets is then returned.

另外,Rust在使用Files和类似的东西时,有一个方便的Result类型。它叫做std::io::Result,当你在使用?对文件进行打开和操作时,通常在main()中看到的就是这个。这其实是一个类型别名。它的样子是这样的:

type Result<T> = Result<T, Error>;

所以这是一个Result<T, Error>,但我们只需要写出Result<T>部分。

现在让我们第一次尝试使用文件。std::fs是处理文件的方法所在,有了std::io::Write,你就可以写。有了它,我们就可以用.write_all()来写进文件。

use std::fs;
use std::io::Write;

fn main() -> std::io::Result<()> {
    let mut file = fs::File::create("myfilename.txt")?; // Create a file with this name.
                                                        // CAREFUL! If you have a file with this name already,
                                                        // it will delete everything in it.
    file.write_all(b"Let's put this in the file")?;     // Don't forget the b in front of ". That's because files take bytes.
    Ok(())
}

然后如果你打开新文件myfilename.txt,会看到内容Let's put this in the file

不过我们不需要写两行,因为我们有?操作符。如果有效,它就会传递我们想要的结果,有点像在迭代器上很多方法一样。这时候?就变得非常方便了。

use std::fs;
use std::io::Write;

fn main() -> std::io::Result<()> {
    fs::File::create("myfilename.txt")?.write_all(b"Let's put this in the file")?;
    Ok(())
}

所以这是说 "请尝试创建一个文件,然后检查是否成功。如果成功了,那就使用.write_all(),然后检查是否成功。"

而事实上,也有一个函数可以同时做这两件事。它叫做std::fs::write。在它里面,你给它你想要的文件名,以及你想放在里面的内容。再次强调,要小心! 如果该文件已经存在,它将删除其中的所有内容。另外,它允许你写一个&str,前面不写b,因为这个:

#![allow(unused)]
fn main() {
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()>
}

AsRef<[u8]>就是为什么你可以给它任何一个。

很简单的:

use std::fs;

fn main() -> std::io::Result<()> {
    fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    Ok(())
}

所以这就是我们要用的文件。这是一个名叫Calvin的漫画人物和他爸爸的对话,他爸爸对他的问题并不认真。有了这个,每次我们都可以创建一个文件来使用。

打开一个文件和创建一个文件一样简单。你只要用open()代替create()就可以了。之后(如果它找到了你的文件),你就可以做read_to_string()这样的事情。要做到这一点,你可以创建一个可变的 String,然后把文件读到那里。它看起来像这样:

use std::fs;
use std::fs::File;
use std::io::Read; // this is to use the function .read_to_string()

fn main() -> std::io::Result<()> {
     fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;


    let mut calvin_file = File::open("calvin_with_dad.txt")?; // Open the file we just made
    let mut calvin_string = String::new(); // This String will hold it
    calvin_file.read_to_string(&mut calvin_string)?; // Read the file into it

    calvin_string.split_whitespace().for_each(|word| print!("{} ", word.to_uppercase())); // Do things with the String now

    Ok(())
}

会打印:

#![allow(unused)]
fn main() {
CALVIN: DAD, HOW COME OLD PHOTOGRAPHS ARE ALWAYS BLACK AND WHITE? DIDN'T THEY HAVE COLOR FILM BACK THEN? DAD: SURE THEY DID. IN 
FACT, THOSE PHOTOGRAPHS *ARE* IN COLOR. IT'S JUST THE *WORLD* WAS BLACK AND WHITE THEN. CALVIN: REALLY? DAD: YEP. THE WORLD DIDN'T TURN COLOR UNTIL SOMETIMES IN THE 1930S...
}

好吧,如果我们想创建一个文件,但如果已经有另一个同名的文件就不做了怎么办?也许你不想为了创建一个新的文件而删除已经存在的其他文件。要做到这一点,有一个结构叫OpenOptions。其实,我们一直在用OpenOptions,却不知道。看看File::open的源码吧。

#![allow(unused)]
fn main() {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().read(true).open(path.as_ref())
    }
}

有意思,这好像是我们学过的建造者模式。File::create也是如此。

#![allow(unused)]
fn main() {
pub fn create<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().write(true).create(true).truncate(true).open(path.as_ref())
    }
}

如果你去OpenOptions的页面,你可以看到所有可以选择的方法。大多数采取bool

  • append(): 意思是 "添加到已经存在的内容中,而不是删除"。
  • create(): 这让 OpenOptions 创建一个文件。
  • create_new(): 意思是只有在文件不存在的情况下才会创建文件。
  • read(): 如果你想让它读取文件,就把这个设置为 true
  • truncate(): 如果你想在打开文件时把文件内容剪为0(删除内容),就把这个设置为true。
  • write(): 这可以让它写入一个文件。

然后在最后你用.open()加上文件名,就会得到一个Result。我们来看一个例子。

// ⚠️
use std::fs;
use std::fs::OpenOptions;

fn main() -> std::io::Result<()> {
     fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    let calvin_file = OpenOptions::new().write(true).create_new(true).open("calvin_with_dad.txt")?;

    Ok(())
}

首先我们用new做了一个OpenOptions(总是以new开头)。然后我们给它的能力是write。之后我们把create_new()设置为true,然后试着打开我们做的文件。打不开,这是我们想要的。

Error: Os { code: 80, kind: AlreadyExists, message: "The file exists." }

让我们尝试使用.append(),这样我们就可以向一个文件写入。为了写入文件,我们可以使用 .write_all(),这是一个尝试写入你给它的一切内容的方法。

另外,我们将使用 write! 宏来做同样的事情。你会记得这个宏,我们在为结构体做impl Display的时候用到过。这次我们是在文件上使用它,而不是在缓冲区上。

use std::fs;
use std::fs::OpenOptions;
use std::io::Write;

fn main() -> std::io::Result<()> {
    fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...")?;

    let mut calvin_file = OpenOptions::new()
        .append(true) // Now we can write without deleting it
        .read(true)
        .open("calvin_with_dad.txt")?;
    calvin_file.write_all(b"And it was a pretty grainy color for a while too.\n")?;
    write!(&mut calvin_file, "That's really weird.\n")?;
    write!(&mut calvin_file, "Well, truth is stranger than fiction.")?;

    println!("{}", fs::read_to_string("calvin_with_dad.txt")?);

    Ok(())
}

这个打印:

Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs *are* in color. It's just the *world* was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s...And it was a pretty grainy color for a while too.
That's really weird.
Well, truth is stranger than fiction.