Rust 語言入門教程:從實戰 To-Do App 開始

image.png

Rust 語言從 2015 年發佈的首個開源版本開始,便得到了社區大量的關注。從 StackOverflow 上的開發者調查來看,Rust 也是 2016 年每一年都最受開發者喜歡的編程語言。html

Rust 由 Mozilla 設計,被定義爲一個系統級編程語言(就像 C 和 C++)。Rust 沒有垃圾處理器,所以性能極爲優良。且其中的一些設計也常讓 Rust 看起來很高級。git

Rust 的學習曲線被廣泛認爲是較爲艱難的。我並非 Rust 語言的深刻了解者,但在這篇教程中,我將嘗試提供一些概念的實用方法,來幫助你更深刻的理解。程序員

咱們將在這篇實戰教程中構建什麼?github

我決定經過遵循 JavaScript 應用的悠久傳統,來將一個 to-do app 當作咱們的第一個 Rust 項目。咱們將重點使用命令行,因此有關命令行的知識必須有所瞭解。同時,你還須要瞭解一些有關編程概念的基礎知識。數據庫

這個程序將基於終端運行。咱們將存儲一些元素的集合,並在其中分別存儲一個表示其活動狀態的布爾值。express

咱們將會圍繞哪些概念來討論?

  • Rust 中的錯誤處理。
  • Options 和 Null 類型。
  • Structs 和 impl。
  • 終端 I/O。
  • 文件系統處理。
  • Rust 中的全部權(Ownership)和借用(borrow)。
  • 匹配模式。
  • 迭代器和閉包。
  • 使用外部的包(crates)。

在咱們開始以前

對於來自 JavaScript 背景的開發者來講,這裏有幾個咱們開始深刻前的建議:npm

  • Rust 是一個強類型的語言。這意味着當編譯器沒法爲咱們推斷類型時,咱們須要時刻關注變量類型。
  • 一樣和 JavaScript 不一樣的是,Rust 中沒有 AFI。這意味着咱們必須主動在語句後鍵入分號 (";")——除非它是函數的最後一條語句(此時能夠省略分號 ; 來將其當作一條 return)。

譯者注:AFI,Automatic semicolon insertion,自動分號插入。JavaScript 能夠不用寫分號,但某些語句也必須使用分號來保證正確地被執行。編程

事不宜遲,讓咱們開始吧!json

Rust 如何從零開始

開始的第一步:下載 Rust 到你的電腦上。想要下載,能夠在 Rust 官方文檔中的入門篇中根據指導來安裝。服務器

譯者注:經過 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 安裝。

在上面的文檔中,你還會找到有關如何將 Rust 與你熟悉的編輯器集成以得到更好開發體驗的相關說明。

除了 Rust 編譯器自己外,Rust 還附帶了一個工具——Cargo。Cargo 是 Rust 的包管理工具,就像 JavaScript 開發者會用到的 npm 和 yarn 同樣。

要開始一個新項目,請先在終端下進入到你想要創造項目的位置,而後只需運行 cargo new <project-name> 便可開始。就我而言,我決定將個人項目命名爲「todo-cli」,因此有了以下命令:

$ cargo new todo-cli
複製代碼

如今切入到新建立的項目目錄並打印出其文件列表。你應該會在其中看到這兩個文件:

$ tree .
.
├── Cargo.toml
└── src
 └── main.rs
複製代碼

在本教程的剩餘篇章中,咱們將會主要關注在 src/main.rs 文件上,因此直接打開這個文件吧。

就像其它衆多的編程語言同樣,Rust 有一個 main 函數來看成一切的入口。fn 來聲明一個函數,同時 println! 中的 ! 符號是一個(macro)。你極可能會立馬看出來,這是 Rust 語言下的一個「Hello World」程序。

想要編譯並運行這個程序,能夠直接直接 cargo run

$ cargo run
Hello world!
複製代碼

如何讀取命令行參數

咱們的目標是讓咱們的 CLI 工具接收兩個參數:第一個參數表明要執行的操做類型,第二個參數表明要操做的對象。

咱們將從讀取並打印用戶輸入的參數開始入手。

使用以下內容替換掉 main 函數裏的內容:

let action = std::env::args().nth(1).expect("Please specify an action");
let item = std::env::args().nth(2).expect("Please specify an item");

println!("{:?}, {:?}", action, item);
複製代碼

來一塊兒消化下代碼裏的重要信息:

  • let [文檔] 給變量綁定一個值
  • std::env::args() [文檔] 是從標準庫的 env 模塊中引入的函數,該函數返回啓動程序時傳遞給其的參數。因爲它是一個迭代器,咱們可使用 nth() 函數來訪問存儲在每一個位置的值。位置 0 引向程序自己,這也是爲何咱們從第一個元素而非第零個元素開始讀取的緣由。
  • expect() [文檔] 是一個 Option 枚舉定義的方法,該方法將返回一個須要給定的值,若是給定的值不存在,則程序當即會被中止,並打印出指定的錯誤信息。

因爲程序能夠不帶參數直接運行,所以 Rust 經過給咱們提供 Option 類型來要求咱們檢查是否確實提供了該值。

做爲開發者,咱們有責任確保在每種條件下都採起適當的措施。

目前咱們的程序中,若是未提供參數,程序會被當即退出。

讓咱們經過以下命令運行程序的同時傳遞兩個參數,記得參數要附加在 -- 以後。

$ cargo run -- hello world!
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running target/debug/todo_cli hello 'world'\!''
"hello", "world!"
複製代碼

如何使用一個自定義類型插入和保存數據

讓咱們考慮一下咱們想在這個程序中實現的目標:可以讀取用戶在命令行輸入的參數,更新咱們的 todo 清單,而後存儲到某個地方來提供記錄。

爲了達到這個目標,咱們將實現自定義類型,來在其中知足咱們的業務。

咱們將使用 Rust 中的 struct(結構體),它使開發者能設計有着更優良結構的代碼,從而避免了必須在主函數中編寫全部的代碼。

如何定義咱們的結構體

因爲咱們將在項目中會用到不少 HashMap,所以咱們能夠考慮將其歸入自定義結構體中。

在文件頂部添加以下行:

use std::collections::HashMap
複製代碼

這將讓咱們能直接地使用 HashMap,而無需每次使用時都鍵入完整的包路徑。

在 main 函數的下方,讓咱們添加如下代碼:

struct Todo {
    // 使用 Rust 內置的 HashMap 來保存 key - val 鍵值對。
    map: HashMap<String, bool>,
}
複製代碼

這將定義出咱們須要的 Todo 類型:一個有且僅有 map 字段的結構體。

這個字段是 HashMap 類型。你能夠將其考慮爲一種 JavaScript 對象,在 Rust 中要求咱們聲明鍵和值的類型。

  • HashMap<String, bool> 表示咱們具備一個字符串組成的鍵,其值是一個布爾值:在應用中來表明當前元素的活動狀態。

如何給咱們的結構體中新增方法

方法就像常規的函數同樣——都是由 fn 關鍵字來聲明,都接受參數且均可以有返回值。

可是,它們與常規函數不一樣之處在於它們是在 struct 上下文中定義的,而且它們的第一個參數始終是 self

咱們將定義一個 impl(實現)代碼塊在上文新增的結構體下方。

impl Todo {
    fn insert(&mut self, key: String) {
        // 在咱們的 map 中新增一個新的元素。
        // 咱們默認將其狀態值設置爲 true
        self.map.insert(key, true);
    }
}
複製代碼

該函數內容十分簡單明瞭:它經過使用 HashMap 內置的 insert 方法將傳入的 key 插入到 map 中。

其中兩個很重要的知識是:

  • mut [doc] 設置一個可變變量

    • 在 Rust 中,每一個變量默認是不可變的。若是你想改變一個值,你須要使用 mut 關鍵字來給相關變量加入可變性。因爲咱們的函數須要經過修改 map 來添加新的值,所以咱們須要將其設置爲可變值。
  • & [doc] 標識一個引用。

    • 你能夠認爲這個變量是一個指針,指向內存中保存這個值的具體地方,並非直接存儲這個值。
    • 在 Rust 屬於中,這被認爲是一個借用(borrow),意味着函數並不擁有該變量,而是指向其存儲位置。

Rust 全部權系統的簡要概覽

有了前面關於借用(borrow)和引用(reference)的知識鋪墊,如今是個很好的時機來簡要地討論 Rust 裏的全部權(ownership)。

全部權是 Rust 中最獨特的功能,它使 Rust 程序員無需手動分配內存(例如在 C/C++ 中)就能夠編寫程序,同時仍能夠在無需垃圾收集器(如 JavaScript 或 Python)的狀況下運行,Rust 會不斷查看程序的內存以釋放未使用的資源。

全部權系統有以下三個規則:

  • Rust 中每一個值都有一個變量:即爲其全部者。
  • 每一個值一次只能有一個全部者。
  • 當全部者超出範圍時,該值將被刪除。

Rust 會在編譯時檢查這些規則,這意味着是否以及什麼時候要在內存中釋放值須要被開發者明確指出。

思考一下以下示例:

fn main() {
  // String 的全部者是 x
  let x = String::from("Hello");

  // 咱們將值移動到此函數中
  // 如今 doSomething 是 x 的全部者
  // 一旦超出 doSomething 的範圍
  // Rust 將釋放與 x 關聯的內存。
  doSomething(x);

  // 因爲咱們嘗試使用值 x,所以編譯器將引起錯誤
  // 由於咱們已將其移至 doSomething 內
  // 咱們此時沒法使用它,由於此時已經沒有全部權
  // 而且該值可能已經被刪除了
  println!("{}", x);
}
複製代碼

在學習 Rust 時,這個概念被普遍地認爲是最難掌握的,由於它對許多程序員來講都是新概念。

你能夠從 Rust 的官方文檔中閱讀有關全部權的更深刻的說明。

咱們不會深刻研究全部權制度的前因後果。如今,請記住我上面提到的規則。嘗試在每一個步驟中考慮是否須要「擁有」這些值後刪除它們,或者是否須要繼續引用它以即可以保留它。

例如,在上面的 insert 方法中,咱們不想擁有 map,由於咱們仍然須要它來將其數據存儲在某個地方。只有這樣,咱們才能最終釋放被分配的內存。

如何將 map 保存到硬盤上

因爲這是一個演示程序,所以咱們將採用最簡單的長期存儲解決方案:將 map 寫入文件到磁盤。

讓咱們在 impl 塊中建立一個新的方法。

impl Todo {
    // [其他代碼]
    fn save(self) -> Result<(), std::io::Error> {
        let mut content = String::new();
        for (k, v) in self.map {
            let record = format!("{}\t{}\n", k, v);
            content.push_str(&record)
        }
        std::fs::write("db.txt", content)
    }
}
複製代碼
  • -> 表示函數返回的類型。咱們在這裏返回的是一個 Result 類型。
  • 咱們遍歷 map,並分別格式化出一個字符串,其中同時包括 key 和 value,並用 tab 製表符分隔,同最後用新的一個換行符來結尾。
  • 咱們將格式化的字符串放入到 content 變量中。
  • 咱們將 content 容寫入名爲 db.txt 的文件中。

值得注意的是,save 擁有自 self 的全部權。此時,若是咱們在調用 save 以後意外嘗試更新 map,編譯器將會阻止咱們(由於 self 的內存將被釋放)。

這是一個完美的例子,展現瞭如何使用 Rust 的內存管理來建立更爲嚴格的代碼,這些代碼將沒法編譯(以防止開發過程當中的人爲錯誤)。

如何在 main 中使用結構體

如今咱們有了這兩種方法,就能夠開始使用了。如今咱們將繼續在以前編寫的 main 函數內編寫功能:若是提供的操做是 add,咱們將該元素插入並存儲到文件中以供將來使用。

將以下代碼添加到以前編寫的兩個參數綁定的下方:

fn main() {
  // ...[參數綁定代碼]

  let mut todo = Todo {
    map: HashMap::new(),
  };
  if action == "add" {
    todo.insert(item);
    match todo.save() {
      Ok(_) => println!("todo saved"),
      Err(why) => println!("An error occurred: {}", why),
    }
  }
}
複製代碼

讓咱們看看咱們都作了什麼:

  • let mut todo = Todo 讓咱們實例化一個結構體,綁定它到一個可變變量上。
  • 咱們經過 . 符號來調用 TODO insert 方法。
  • 咱們將匹配 save 功能返回的結果,並在不一樣狀況下載屏幕上顯示一條消息。

讓咱們測試運行吧。打開終端並輸入:

$ cargo run -- add "code rust"
todo saved
複製代碼

讓咱們來檢查元素是否真的保存了:

$ cat db.txt
code rust true
複製代碼

你能夠在這個 gist 中找到完整的代碼片斷。

如何讀取文件

如今咱們的程序有個根本性的缺陷:每次「add」添加時,咱們都會重寫整個 map 而不是對其進行更新。這是由於咱們在程序運行的每一次都創造一個全新的空 map 對象,如今一塊兒來修復它。

在 TODO 中新增 new 方法

咱們將爲 Todo 結構實現一個新的功能。調用後,它將讀取文件的內容,並將已存儲的值返回給咱們的 Todo。請注意,這不是一個方法,由於它沒有將 self 做爲第一個參數。

咱們將其稱爲 new,這只是一個 Rust 約定(請參閱以前使用的 HashMap::new())。

讓咱們在 impl 塊中添加如下代碼:

impl Todo {
  fn new() -> Result<Todo, std::io::Error> {
    let mut f = std::fs::OpenOptions::new()
      .write(true)
      .create(true)
      .read(true)
      .open("db.txt")?;
    let mut content = String::new();
    f.read_to_string(&mut content)?;
    let map: HashMap<String, bool> = content
      .lines()
      .map(|line| line.splitn(2, '\t').collect::<Vec<&str>>())
      .map(|v| (v[0], v[1]))
      .map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))
      .collect();
    Ok(Todo { map })
  }

// ...剩餘的方法
}
複製代碼

若是看到上面的代碼感到頭疼的話,請不用擔憂。咱們這裏使用了一種更具函數式的編程風格,主要是用來展現 Rust 支持許多其餘語言的範例,例如迭代器,閉包和 lambda 函數。

讓咱們看看上面代碼都具體發生了什麼:

  • 咱們定義了一個 new 函數,其會返回一個 Result 類型,要麼是 Todo 結構體要麼是 io:Error

  • 咱們經過定義各類 OpenOptions 來配置如何打開「db.txt」。最顯著的是 create(true) 標誌,這表明若是該文件不存在則建立這個文件。

  • f.read_to_string(&mut content)? 讀取文件中的全部字節,並將它們附加到 content 字符串中。

    • 注意:記得添加使用 std:io::Read 在文件的頂部以及其餘 use 語句來使用 read_to_string 方法。
  • 咱們須要將文件中的 String 類型轉換爲 HashMap。爲此咱們將 map 變量與此行綁定:let map: HashMap<String, bool>

    • 這是編譯器在爲咱們推斷類型時遇到麻煩的狀況之一,所以咱們須要自行聲明。
  • lines [文檔] 在字符串的每一行上建立一個 Iterator 迭代器,來在文件的每一個條目中進行迭代。由於咱們已在每一個條目的末尾使用了 /n 格式化。

  • map [文檔] 接受一個閉包,並在迭代器的每一個元素上調用它。

  • line.splitn(2, '\t') [文檔] 將咱們的每一行經過 tab 製表符切割。

  • collect::<Vec<&str>>() [文檔] 是標準庫中最強大的方法之一:它將迭代器轉換爲相關的集合。

    • 在這裏,咱們告訴 map 函數經過將 ::Vec<&str> 附加到方法中來將咱們的 Split 字符串轉換爲借來的字符串切片的 Venctor,這回告訴編譯器在操做結束時須要哪一個集合。
  • 而後爲了方便起見,咱們使用 .map(|v| (v[0], v[1])) 將其轉換爲元祖類型。

  • 而後使用 .map(|(k, v)| (String::from(k), bool::from_str(v).unwrap())) 將元祖的兩個元素轉換爲 String 和 boolean。

    • 注意:記得添加 use std::str::FromStr; 在文件頂部以及其它 use 語句,以便可以使用 from_str 方法。
  • 咱們最終將它們收集到咱們的 HashMap 中。此次咱們不須要聲明類型,由於 Rust 從綁定聲明中推斷出了它。

  • 最後,若是咱們從未遇到任何錯誤,則使用 Ok(Todo { map }) 將結果返回給調用方。

    • 注意,就像在 JavaScript 中同樣,若是鍵和變量在結構內具備相同的名稱,則可使用較短的表示法。

phew!

image

你作的很棒!圖片來源於 rustacean.net/。

另外一種等價方式

儘管一般認爲 map 更爲好用,但以上內容也能夠經過基本的 for 循環來使用。你能夠選擇本身喜歡的方式。

fn new() -> Result<Todo, std::io::Error> {
  // 打開 db 文件
  let mut f = std::fs::OpenOptions::new()
    .write(true)
    .create(true)
    .read(true)
    .open("db.txt")?;
  // 讀取其內容到一個新的字符串中
  let mut content = String::new();
  f.read_to_string(&mut content)?;
  
  // 分配一個新的空的 HashMap
  let mut map = HashMap::new();
  
  // 遍歷文件中的每一行
  for entries in content.lines() {
    // 分割和綁定值
    let mut values = entries.split('\t');
    let key = values.next().expect("No Key");
    let val = values.next().expect("No Value");
    // 將其插入到 HashMap 中
    map.insert(String::from(key), bool::from_str(val).unwrap());
  }
  // 返回 Ok
  Ok(Todo { map })
}
複製代碼

上述代碼和以前的函數式代碼是功能性等價的關係。

如何使用這個新方法

在 main 中,只須要用如下代碼塊來初始化 todo 變量:

let mut todo = Todo::new().expect("Initialisation of db failed");
複製代碼

如今若是咱們回到終端並執行若干個以下「add」命令,咱們應該能夠看到咱們的數據庫被正確的更新了。

$ cargo run -- add "make coffee"
todo saved
$ cargo run -- add "make pancakes"
todo saved
$ cat db.txt
make coffee     true
make pancakes   true
複製代碼

你能夠在這個 gist 中找到目前階段下全部的完整代碼。

如何在集合中更新一個值

正如全部的 todo app 同樣,咱們但願不只可以添加項目,並且可以對齊進行狀態切換並將其標記爲已完成。

如何新增 complete 方法

咱們須要在 Todo 結構體中新增一個 complete 方法。在其中,咱們獲取到 key 的引用值,並更新其值。在 key 不存在的狀況下,返回 None

impl Todo {
  // [其他的 TODO 方法]

  fn complete(&mut self, key: &String) -> Option<()> {
    match self.map.get_mut(key) {
      Some(v) => Some(*v = false),
      None => None,
    }
  }
}
複製代碼

讓咱們看看上面代碼發生了什麼:

  • 咱們聲明瞭方法的返回類型:一個空的 Option
  • 整個方法返回 Match 表達式的結果,該結果將爲空 Some() None
  • 咱們使用 * [文檔] 運算符來取消引用該值,並將其設置爲 false。

如何使用 complete 方法

咱們能夠像以前使用 insert 同樣使用 「complete」 方法。

main 函數中,咱們使用 else if 語句來檢查命令行傳遞的動做是不是「complete」。

// 在 main 函數中

if action == "add" {
    // add 操做的代碼
} else if action == "complete" {
    match todo.complete(&item) {
        None => println!("'{}' is not present in the list", item),
        Some(_) => match todo.save() {
            Ok(_) => println!("todo saved"),
            Err(why) => println!("An error occurred: {}", why),
        },
    }
}
複製代碼

是時候來分析咱們在上述代碼中作的事了:

  • 若是咱們檢測到返回了 Some 值,則調用 todo.save 將更改永久存儲到咱們的文件中。

  • 咱們匹配由 todo.complete(&item) 方法返回的 Option。

  • 若是返回結果爲 None,咱們將向用戶打印警告,來提供良好的交互性體驗。

    • 咱們經過 &item 將 item 做爲引用傳遞給「todo.complete」方法,以便 main 函數仍然擁有該值。這意味着咱們能夠再接下來的 println! 宏中繼續使用到這個變量。
    • 若是咱們不這樣作,那麼該值將由「complete」用於,最終被意外丟棄。
  • 若是咱們檢測到返回了 Some 值,則調用 todo.save 將這次更改永久存儲到咱們的文件中。

和以前同樣,你能夠在這個 gist 中找到目前階段下的全部相關代碼。

運行這個程序吧

如今是時候在終端來完整運行咱們開發的這個程序了。讓咱們經過先刪除掉以前的 db.txt 來從零開始這個程序:

$ rm db.txt
複製代碼

而後在 todos中進行新增和修改操做:

$ cargo run -- add "make coffee"
$ cargo run -- add "code rust"
$ cargo run -- complete "make coffee"
$ cat db.txt
make coffee     false
code rust       true
複製代碼

這意味着在這些命令執行完成後,咱們將會獲得一個完成的元素(「make coffee」),和一個還沒有完成的元素(「code rust」)。

假設咱們此時再從新新增一個喝咖啡的元素「make coffee」:

$ cargo run -- add "make coffee"
$ cat db.txt
make coffee     true
code rust       true
複製代碼

番外:如何使用 Serde 將其存儲爲 JSON

該程序即便很小,但也能正常運行了。此外,咱們能夠稍微改變一些邏輯。對於來自 JavaScript 世界的我,決定將值存儲爲 JSON 文件而不是純文本文件。

咱們將藉此機會瞭解如何安裝和使用來自 Rust 開源社區的名爲 creates.io 的軟件包。

如何安裝 serde

要將新的軟件包安裝到咱們的項目中,請打開 cargo.toml 文件。在底部,你應該會看到一個 [dependencies] 字段:只須要將如下內容添加到文件中:

[dependencies]
serde_json = "1.0.60"
複製代碼

這就夠了。下次咱們運行程序的時候,cargo 將會編譯咱們的程序並下載和導入這個新的包到咱們的項目之中。

如何改動 Todo::New

咱們要使用 Serde 的第一個地方是在讀取 db 文件時。如今,咱們要讀取一個 JSON 文件而非「.txt」文件。

impl 代碼塊中,咱們更像一下 new 方法:

// 在 Todo impl 代碼塊中

fn new() -> Result<Todo, std::io::Error> {
  // 打開 db.json
  let f = std::fs::OpenOptions::new()
    .write(true)
    .create(true)
    .read(true)
    .open("db.json")?;
  // 序列化 json 爲 HashMap
  match serde_json::from_reader(f) {
    Ok(map) => Ok(Todo { map }),
    Err(e) if e.is_eof() => Ok(Todo {
      map: HashMap::new(),
    }),
    Err(e) => panic!("An error occurred: {}", e),
  }
}
複製代碼

值得注意的改動是:

  • 文件選項再也不須要 mut f 來綁定,由於咱們不須要像之前同樣手動將內容分配到 String 中。Serde 會來處理相關邏輯。

  • 咱們將文件拓展名更新爲了 db.json

  • serde_json::from_reader [文檔] 將爲咱們反序列化文件。它會干擾 map 的返回類型,並會嘗試將 JSON 轉換爲兼容的 HashMap。若是一切順利,咱們將像之前同樣返回 Todo 結構。

  • Err(e) if e.is_eof() 是一個匹配守衛,可以讓咱們優化 Match 語句的行爲。

    • 若是 Serde 做爲錯誤返回一個過早的 EOF(文件結尾),則意味着該文件徹底爲空(例如,在第一次運行時,或若是咱們刪除了該文件)。在那種狀況下,咱們從錯誤中恢復並返回一個空的 HashMap。
  • 對於其它全部錯誤,程序會當即被中斷退出。

如何改動 Todo.save

咱們要使用 Serde 的另外一個地方是將 map 另存爲 JSON。爲此,將 impl 塊中的 save 方法更新爲:

// 在 Todo impl 代碼塊中
fn save(self) -> Result<(), Box<dyn std::error::Error>> {
  // 打開 db.json
  let f = std::fs::OpenOptions::new()
    .write(true)
    .create(true)
    .open("db.json")?;
  // 經過 Serde 寫入文件
  serde_json::to_writer_pretty(f, &self.map)?;
  Ok(())
}
複製代碼

和之前同樣,讓咱們看看這裏所作的更改:

  • Box<dyn std::error::Error>。此次咱們返回一個包含 Rust 通用錯誤實現的 Box

    • 簡而言之,Box 是指向內存中分配的指針。
    • 因爲打開文件時可能會返回 Serde 錯誤,因此咱們實際上並不知道函數會返回這兩個錯誤裏的哪個。
    • 所以咱們須要返回一個指向可能錯誤的指針,而不是錯誤自己,以便調用者處理它們。
  • 咱們固然已經將文件名更新爲 db.json 以匹配文件名。

  • 最後,咱們讓 Serde 承擔繁重的工做:將 HashMap 編寫爲 JSON 文件。

  • 請記得從文件頂部刪除 use std::io::Read;use std::str::FromStr;,由於咱們再也不須要它們了。

這就搞定了。

如今你能夠運行你的程序並檢查輸出是否保存到文件中。若是一切都很順利,你會看到你的 todos 都保持爲 JSON 了。

你能夠在這個 gist 中閱讀當前階段下完整的代碼。

結語、技巧和更多資源

這是一段漫長的旅程,很榮幸你能閱讀到這裏。

我但願你能在這個教程中學到一些東西,併產生了更多的好奇心。別忘了咱們在這裏介紹的是一門很是「底層」的語言。

這是 Rust 吸引個人重要緣由——Rust 使我可以編既快速又具備內存效率的代碼,而沒必要畏懼承擔過多的編碼責任:我知道編譯器會幫我優化更多,在運行前可能會出現錯誤的狀況下提早中斷運行。

在結束前,我想向你分享一些其餘技巧和資源,以幫助你在 Rust 的旅途中繼續前行:

  • Rust fmt 是一個很是方便的工具,你能夠按照一致的模式運行以格式化代碼。沒必要再浪費時間配置你喜歡的 linter 插件。
  • cargo check [文檔] 將嘗試在不運行的狀況下編譯代碼:這在你只想在不實際運行時檢查代碼正確性的狀況下,會變得頗有用。
  • Rust 帶有集成的測試套件和生成文檔的工具:cargo testcargo doc。此次咱們沒有涉及它們,由於本教程內容量已經足夠多了,或許將來會有所涉及。

想要了解有關 Rust 的更多內容,我認爲這些資源真的很棒:

  • 官方 Rust 網站,全部重要信息的彙集地。
  • 若是你喜歡經過聊天來互動交流,Rust 的 Discord 服務器是個很活躍和有用的社區。
  • 若是你想要經過讀書來學習,「Rust 程序設計語言」一書是個很好的選擇。
  • 若是你更喜歡視頻類型的資料,Ryan Levick 的「Rust 介紹」視頻系列是個很棒的資源。

你能夠在 Github 中找到本文的相關源碼。

文中的插圖來自於 rustacean.net/。

感謝閱讀,祝你編碼愉快!

相關文章
相關標籤/搜索