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