[譯]使用 Rust 開發一個簡單的 Web 應用,第 2a 部分

LeopPro發表於2018-02-12

使用 Rust 開發一個簡單的 Web 應用,第 2a 部分

1 來龍去脈

如果你還沒看過這個系列的第一部分,請從這裡開始。

在第一部分,我們成功的建立了一個 Rust 工程並且編寫了一個“Hello World” Web 應用。

起初,在這個部分中我想寫一個可以將日期寫入檔案系統的程式。但是在這個過程中,我和型別檢查鬥爭了好久,所以這個部分主要寫這個。

2 開始

上一次還不算太糟。但當我之前做這部分的時候,我記得這是最難的部分。

讓我們從移動已存在的 main.rs 開始,這樣我們就可以使用一個新檔案。

$ pwd
/Users/joel/Projects/simple-log
$ cd src/
$ ls
main.rs
$ mv main.rs web_main.rs
$ touch main.rs
複製程式碼

3 回憶“Hello World”

我可以不借助任何參考獨立寫出“Hello World”麼?

讓我試試:

fn main() {
    println!("Hello, world");
}
複製程式碼

然後:

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
     Running `target/debug/simple-log`
Hello, world
複製程式碼

哦,我想我還記得它。我只是有一點不太肯定,我是否需要為 println! 匯入些什麼,現在看來,這是不必要的。

4 天真的方法

好了,繼續。上網搜尋“Rust 建立檔案”,我找到了 std::fs::Filedoc.rust-lang.org/std/fs/stru…。讓我們來試試一個例子:

use std::fs::File;

fn main() {
    let mut f = try!(File::create("foo.txt"));
}
複製程式碼

編譯:

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
<std macros>:5:8: 6:42 error: mismatched types:
 expected `()`,
    found `core::result::Result<_, _>`
(expected (),
    found enum `core::result::Result`) [E0308]
<std macros>:5 return $ crate:: result:: Result:: Err (
<std macros>:6 $ crate:: convert:: From:: from ( err ) ) } } )
<std macros>:1:1: 6:48 note: in expansion of try!
src/main.rs:5:17: 5:46 note: expansion site
error: aborting due to previous error
Could not compile `simple-log`.
複製程式碼

當我編寫第一個版本的時候,解決這個錯誤真是花費了我很多時間。我不再經常活躍於社群,所以這些問題的解決方案可能是比較粗糙的。弄清楚這個問題給我留下了深刻的印象,所以我馬上就知道答案了。

上面程式碼的問題是 try! 巨集的展開程式碼在出現錯誤的情況下返回 Err 型別。然而 main 返回單元型別(()1,這會導致一個型別錯誤。

我認為有三點難以理解:

  1. 在這一點上,我不是很確定要如何理解錯誤資訊。'expected' 和 'found'都指什麼?我知道答案之後,我明白 'expected' 指的是 main 的返回值,我可以清楚的明白 'expected' 和 'found'。
  2. 於我而言,查閱文件並沒有讓我立刻明白 try! 是如何影響呼叫它的函式返回值的。當然,我應該查閱 return 在巨集中的定義。然而,當我找到一個在 Rust 文件 中的評論時,我才明白在這個例子中為什麼 try! 不能在 main 中呼叫。
  3. 這個錯誤事實上可以在巨集中體現。我當時沒明白,但 Rust 編譯器可以輸出經過巨集擴充套件後的原始碼。這個功能讓這類問題易於除錯。

在第三點中,我們提到了擴充套件巨集。查閱擴充套件巨集是除錯這類問題非常有效的方法,這值得我們深究。

5 除錯擴充套件巨集

首先,我通過搜尋“Rust 擴充套件巨集”查閱到這些程式碼:

use std::fs::File;

fn main() {
    let mut f = try!(File::create("foo.txt"));
}
複製程式碼

……我們可以通過這種方式使編譯器輸出巨集擴充套件:

$ rustc src/main.rs --pretty=expanded -Z unstable-options
#![feature(no_std)]
#![no_std]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std as std;
use std::fs::File;

fn main() {
    let mut f =
        match File::create("foo.txt") {
            ::std::result::Result::Ok(val) => val,
            ::std::result::Result::Err(err) => {
                return ::std::result::Result::Err(::std::convert::From::from(err))
            }
        };
}
複製程式碼

這次就易於除錯了。巨集是非常有效的工具,但是同其他工具一樣,我們要知道何時、怎樣使用它們。

看看吧,上面輸出中的 return 定義,問題就出在這裡。它嘗試返回一個 Err 結果,但是 main 函式期望得到一個單元型別。

6 和型別鬥爭

我要知道如何解決這個型別問題。我嘗試模仿 try! 巨集,但是這次只返回單元型別:

use std::fs::File;

fn main() {
    match File::create("foo.txt") {
        Ok(val) => val,
        Err(err) => ()
    }
}
複製程式碼

執行:

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:5:5: 8:6 error: match arms have incompatible types:
 expected `std::fs::File`,
    found `()`
(expected struct `std::fs::File`,
    found ()) [E0308]
src/main.rs:5     match File::create("foo.txt") {
src/main.rs:6         Ok(val) => val,
src/main.rs:7         Err(err) => ()
src/main.rs:8     }
src/main.rs:7:21: 7:23 note: match arm with an incompatible type
src/main.rs:7         Err(err) => ()
                                  ^~
error: aborting due to previous error
Could not compile `simple-log`.
複製程式碼

我不知道該如何表述,“這裡不應該做任何事”。我猜 val 一定是 std::fs::File 型別,所以編譯器推斷 match所有分支都應該返回它。我可以令 Ok 分支也不返回任何東西麼?

use std::fs::File;

fn main() {
    match File::create("foo.txt") {
        Ok(val) => (),
        Err(err) => ()
    }
}
複製程式碼

執行:

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:6:12: 6:15 warning: unused variable: `val`, #[warn(unused_variables)] on by default
src/main.rs:6         Ok(val) => (),
                         ^~~
src/main.rs:7:13: 7:16 warning: unused variable: `err`, #[warn(unused_variables)] on by default
src/main.rs:7         Err(err) => ()
                          ^~~
     Running `target/debug/simple-log`
$ ls
Cargo.lock      Cargo.toml      foo.txt         src             target
複製程式碼

它建立了 foo.txt!當然,程式碼可以更優雅,但是它現在很不錯。讓我們試試其他的方法:

use std::fs::File;

fn main() {
    File::create("foo.txt")
}
複製程式碼

=>

$ rm foo.txt
$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:5:5: 5:28 error: mismatched types:
 expected `()`,
    found `core::result::Result<std::fs::File, std::io::error::Error>`
(expected (),
    found enum `core::result::Result`) [E0308]
src/main.rs:5     File::create("foo.txt")
                  ^~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `simple-log`.

To learn more, run the command again with --verbose.
複製程式碼

我之前看到過這個。它的意思是 main 函式返回了 File::create 的結果。我想,這裡不應該返回任何東西,但是我沒有往這個方向思考。如果我新增一個分號呢?

use std::fs::File;

fn main() {
    File::create("foo.txt");
}
複製程式碼

=>

$ rm foo.txt
$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:5:5: 5:29 warning: unused result which must be used, #[warn(unused_must_use)] on by default

src/main.rs:5     File::create("foo.txt");
                  ^~~~~~~~~~~~~~~~~~~~~~~~
     Running `target/debug/simple-log`
$ ls
Cargo.lock      Cargo.toml      foo.txt         src             target
複製程式碼

好了,我們成功執行且建立了檔案,但是現在有一個“未使用的結果”警告。讓我們去做點什麼來處理這個結果:

use std::fs::File;

fn main() {
    match File::create("foo.txt") {
        Ok(val) => println!("File created!"),
        Err(err) => println!("Error: could not create file.")
    }
}
複製程式碼

=>

$ rm foo.txt
$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:6:12: 6:15 warning: unused variable: `val`, #[warn(unused_variables)] on by default
src/main.rs:6         Ok(val) => println!("File created!"),
                         ^~~
src/main.rs:7:13: 7:16 warning: unused variable: `err`, #[warn(unused_variables)] on by default
src/main.rs:7         Err(err) => println!("Error: could not create file.")
                          ^~~
     Running `target/debug/simple-log`
File created!
複製程式碼

現在出現了未使用的變數警告。我的直覺是,我們可以使用省略號或刪除變數名來解決這個問題:

use std::fs::File;

fn main() {
    match File::create("foo.txt") {
        Ok(..) => println!("File created!"),
        Err(..) => println!("Error: could not create file.")
    }
}
複製程式碼

=>

cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
     Running `target/debug/simple-log`
File created!
複製程式碼

看,使用省略號可行,那麼如果我刪除省略號會發生什麼?

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:6:12: 6:13 error: nullary enum variants are written with no trailing `( )`
src/main.rs:6         Ok() => println!("File created!"),
                         ^
src/main.rs:7:13: 7:14 error: nullary enum variants are written with no trailing `( )`
src/main.rs:7         Err() => println!("Error: could not create file.")
                          ^
error: aborting due to 2 previous errors
Could not compile `simple-log`.
複製程式碼

這和我預想的不一樣。我猜測“nullary”的意思是空元組,它需要被刪除。如果我把括號刪掉:

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:6:9: 6:11 error: this pattern has 0 fields, but the corresponding variant has 1 field [
E0023]
src/main.rs:6         Ok => println!("File created!"),
                      ^~
src/main.rs:7:9: 7:12 error: this pattern has 0 fields, but the corresponding variant has 1 field [
E0023]
src/main.rs:7         Err => println!("Error: could not create file.")
                      ^~~
error: aborting due to 2 previous errors
Could not compile `simple-log`.

To learn more, run the command again with --verbose.
複製程式碼

這很好理解,基本上是我所預想的。我的思維正在形成!

7 寫入檔案

讓我們嘗試一些更難的東西。這個怎麼樣:

  1. 嘗試建立日誌檔案。如果日誌檔案不存在則建立它。
  2. 嘗試寫一個字串到日誌檔案。
  3. 把一切理清。

第一個例子進度還沒有過半,我們繼續:

use std::fs::File;

fn log_something(filename, string) {
    let mut f = try!(File::create(filename));
    try!(f.write_all(string));
}

fn main() {
    match log_something("log.txt", "ITS ALIVE!!!") {
        Ok(..) => println!("File created!"),
        Err(..) => println!("Error: could not create file.")
    }
}
複製程式碼

=>

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:3:26: 3:27 error: expected one of `:` or `@`, found `,`
src/main.rs:3 fn log_something(filename, string) {
                                       ^
Could not compile `simple-log`.

To learn more, run the command again with --verbose.
$
複製程式碼

所以我想函式引數必須要宣告型別:

use std::fs::File;

fn log_something(filename: &'static str, string: &'static str) {
    let mut f = try!(File::create(filename));
    try!(f.write_all(string));
}

fn main() {
    match log_something("log.txt", "ITS ALIVE!!!") {
        Ok(..) => println!("File created!"),
        Err(..) => println!("Error: could not create file.")
    }
}
複製程式碼

=>

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
<std macros>:5:8: 6:42 error: mismatched types:
 expected `()`,
    found `core::result::Result<_, _>`
(expected (),
    found enum `core::result::Result`) [E0308]
<std macros>:5 return $ crate:: result:: Result:: Err (
<std macros>:6 $ crate:: convert:: From:: from ( err ) ) } } )
<std macros>:1:1: 6:48 note: in expansion of try!
src/main.rs:4:17: 4:45 note: expansion site
src/main.rs:5:12: 5:29 error: type `std::fs::File` does not implement any method in scope named `wr
ite_all`
src/main.rs:5     try!(f.write_all(string));
                         ^~~~~~~~~~~~~~~~~
<std macros>:1:1: 6:48 note: in expansion of try!
src/main.rs:5:5: 5:31 note: expansion site
src/main.rs:5:12: 5:29 help: methods from traits can only be called if the trait is in scope; the f
ollowing trait is implemented but not in scope, perhaps add a `use` for it:
src/main.rs:5:12: 5:29 help: candidate #1: use `std::io::Write`
<std macros>:5:8: 6:42 error: mismatched types:
 expected `()`,
    found `core::result::Result<_, _>`
(expected (),
    found enum `core::result::Result`) [E0308]
<std macros>:5 return $ crate:: result:: Result:: Err (
<std macros>:6 $ crate:: convert:: From:: from ( err ) ) } } )
<std macros>:1:1: 6:48 note: in expansion of try!
src/main.rs:5:5: 5:31 note: expansion site
src/main.rs:10:9: 10:15 error: mismatched types:
 expected `()`,
    found `core::result::Result<_, _>`
(expected (),
    found enum `core::result::Result`) [E0308]
src/main.rs:10         Ok(..) => println!("File created!"),
                       ^~~~~~
src/main.rs:11:9: 11:16 error: mismatched types:
 expected `()`,
    found `core::result::Result<_, _>`
(expected (),
    found enum `core::result::Result`) [E0308]
src/main.rs:11         Err(..) => println!("Error: could not create file.")
                       ^~~~~~~
error: aborting due to 5 previous errors
Could not compile `simple-log`.

To learn more, run the command again with --verbose.
複製程式碼

出現了好多錯誤。我們看第一個錯誤,我猜 log_something 函式需要規定一個返回值。我嘗試了一些方法,但是我卡住了。求助於搜尋引擎!

幾分鐘過去了,我終於找到了答案。我在 GitHub 上找到了一些相關資訊,但是它並不有效。我大約嘗試了 50 種不同方案,最終得以解決:

use std::io::prelude::*;
use std::fs::File;

fn log_something(filename: &'static str, string: &'static str) -> Result<File,std::io::error::Error> {
    let mut f = try!(File::create(filename));
    try!(f.write_all(string));
}

fn main() {
    match log_something("log.txt", "ITS ALIVE!!!") {
        Ok(..) => println!("File created!"),
        Err(..) => println!("Error: could not create file.")
    }
}
複製程式碼

我不知道它能正常工作的原因。如果我理解的正確,返回值的型別 Result 的引數應該是 Filestd::io::error::Error。這究竟是什麼意思?對我來說,這兩種型別很奇怪,一種是實際結果(檔案),另一種是 Error 型別。為什麼?我想,我修復了剩餘錯誤之後,這還需要再次修復。

現在當我嘗試執行它時,我得到如下錯誤資訊:

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:8:22: 8:28 error: mismatched types:
 expected `&[u8]`,
    found `&'static str`
(expected slice,
    found str) [E0308]
src/main.rs:8     try!(f.write_all(string));
                                   ^~~~~~
<std macros>:1:1: 6:48 note: in expansion of try!
src/main.rs:8:5: 8:31 note: expansion site
error: aborting due to previous error
Could not compile `simple-log`.

To learn more, run the command again with --verbose.
複製程式碼

我在例子中看到他們在字串前加了一個 b Ok,我忽略它只是為了看看會發生什麼。修復引數:

use std::io::prelude::*;
use std::fs::File;

fn log_something(filename: &'static str, string: &'static [u8; 12]) -> Result<File,std::io::error::Error> {
    let mut f = try!(File::create(filename));
    try!(f.write_all(string));
}

fn main() {
    match log_something("log.txt", "ITS ALIVE!!!") {
        Ok(..) => println!("File created!"),
        Err(..) => println!("Error: could not create file.")
    }
}
複製程式碼

=>

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:4:85: 4:106 error: struct `Error` is private
src/main.rs:4 fn log_something(filename: &'static str, string: &'static [u8; 12]) -> Result<File, std::io::error::Error> {
                                                                                                  ^~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `simple-log`.
複製程式碼

我知道這將出現問題。花一些時間查閱資料。

Rust 文件有一章介紹 Result。看起來我做了一些非常規的操作。我是說,這似乎是“最好”的方式來處理當前的錯誤,但是我很疑惑。我曾見過這個 unwrap 幾次,這看起來可能是我想要的。如果我嘗試 unwrap,事情可能會有所不同:

fn log_something(filename: &'static str, string: &'static [u8; 12]) {
    let mut f = File::create(filename).unwrap();
    f.write_all(string);
}

fn main() {
    log_something("log.txt", b"ITS ALIVE!!!")
}
複製程式碼

=>

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:6:5: 6:25 warning: unused result which must be used, #[warn(unused_must_use)] on by def
ault
src/main.rs:6     f.write_all(string);
                  ^~~~~~~~~~~~~~~~~~~~
     Running `target/debug/simple-log`
$ ls
Cargo.lock      Cargo.toml      foo.txt         log.txt         src             target
$ cat log.txt
ITS ALIVE!!!
複製程式碼

看,雖然有一個警告,它還是工作了。但我想,這不是 Rust 的方式,Rust 提倡提前失敗或丟擲錯誤。

真正的問題是 try!try! 巨集返回古怪 Result 的分支:

return $crate::result::Result::Err($crate::convert::From::from(err))
複製程式碼

這意味著無論我傳入什麼都必須在列舉上實現一個 From::from 特徵。但是我真的不知道特徵或列舉是如何工作的,而且我認為整個事情對於我來說都是矯枉過正。

我去查閱 Result 的文件,看起來我走錯了方向:doc.rust-lang.org/std/result/。這裡的 io::Result 例子似乎於我做的相似,所以讓我看看我是否能解決問題:

use std::io::prelude::*;
use std::fs::File;
use std::io;

fn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> {
    let mut f = try!(File::create(filename));
    try!(f.write_all(string));
}

fn main() {
    match log_something("log.txt", b"ITS ALIVE!!!") {
        Ok(..) => println!("File created!"),
        Err(..) => println!("Error: could not create file.")
    }
}
複製程式碼

=>

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:5:1: 8:2 error: not all control paths return a value [E0269]
src/main.rs:5 fn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()>
 {
src/main.rs:6     let mut f = try!(File::create(filename));
src/main.rs:7     try!(f.write_all(string));
src/main.rs:8 }
error: aborting due to previous error
Could not compile `simple-log`.

To learn more, run the command again with --verbose.
複製程式碼

經過一段時間的思考,我發現了問題:必須在 log_something 的最後新增 OK(()) 語句。我通過參考 Result 文件得出這樣的結論。

我已經習慣了在函式最後的無分號語句意思是 return ();而錯誤訊息“不是所有的分支都返回同一型別”不好理解 —— 對我來說,這是型別不匹配問題。當然,() 可能不是一個值,我仍然認為這是令人困惑的。

我們最終的結果(這篇文章):

use std::io::prelude::*;
use std::fs::File;
use std::io;

fn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> {
    let mut f = try!(File::create(filename));
    try!(f.write_all(string));
    Ok(())
}

fn main() {
    match log_something("log.txt", b"ITS ALIVE!!!") {
        Ok(..) => println!("File created!"),
        Err(..) => println!("Error: could not create file.")
    }
}
複製程式碼

=>

$ rm log.txt
$ cargo run
     Running `target/debug/simple-log`
File created!
$ cat log.txt
ITS ALIVE!!!
複製程式碼

好了,它工作了,美滋滋!我想在這裡結束本章,因為本章內容非常富有挑戰性。我確信這個程式碼是可以做出改進的,但這裡暫告一段落,下一章我們將研究 Rust 中的日期和時間.

8 更新

  1. NMSpaz 在 Reddit 指出了我例子中的一個錯誤。

系列文章:使用 Rust 開發一個簡單的 Web 應用

腳註:

1 哈哈哈哈。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章