使用檔案

現在我們正在電腦上使用 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.

另外,在使用 File 和類似的東西時,Rust 有個方便的 Result 型別叫做 std::io::Result。在 main() 中當你使用 ? 在開啟和操作檔案時,通常看到的就是這個。這其實是類型別名 (type alias)。像這樣:

#![allow(unused)]
fn 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")?; // 用這個名稱建立檔案.
                                                        // 小心! 如果你有已經有個同名的檔案,
                                                        // 它會刪除檔案裡面所有內容.
    file.write_all(b"Let's put this in the file")?;     // 別忘記在 " 前面的 b. 那是因為檔案接受位元組資料.
    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; // 這是為了要使用 .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")?; // 開啟我們做的檔案
    let mut calvin_string = String::new(); // 這個 String 會保留讀取內容
    calvin_file.read_to_string(&mut calvin_string)?; // 讀取檔案到 String 裡

    calvin_string.split_whitespace().for_each(|word| print!("{} ", word.to_uppercase())); // 現在用 String 做些事

    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...

好吧,要是我們想建立檔案,但如果已經有同名的檔案就不要這樣做該怎麼辦?也許你不想為了建立新的檔案而刪除已經存在的其他檔案。要做到這一點,有個結構叫 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 的時候。而這次我們是在檔案上使用它,不是在緩衝區 (buffer) 上。

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) // 現在我們可以繼續寫入而不用刪除檔案
        .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.