banner
魔法少女秋小日

Qiuxiaori

github
email

《Rust 語言聖經》學習筆記

在線閱讀地址:Rust 語言聖經

閱讀進度:60%

image


開發工具#

VSCode#

插件

RustRover#

插件

設置

  • 代碼格式

image.png

image.png


Cargo#

使用 cargo 工具的最大優勢就在於,能夠對該項目的各種依賴項進行方便、統一和靈活的管理。

Cargo.toml 和 Cargo.lock#

  • Cargo.tomlcargo 特有的項目數據描述文件。它存儲了項目的所有元配置信息,如果 Rust 開發者希望 Rust 項目能夠按照期望的方式進行構建、測試和運行,那麼,必須按照合理的方式構建 Cargo.toml
  • Cargo.lock 文件是 cargo 工具根據同一項目的 toml 文件生成的項目依賴詳細清單,因此我們一般不用修改它,只需要對著 Cargo.toml 文件撸就行了。

Cargo.toml 中,主要通過各種依賴段落來描述該項目的各種依賴項:

[dependencies]
rand = "0.3"
hammer = { version = "0.5.0"} // 基於 Rust 官方倉庫 crates.io,通過版本說明來描述
color = { git = "https://github.com/bjz/color-rs" } // 基於項目源代碼的 git 倉庫地址,通過 URL 來描述
geometry = { path = "crates/geometry" } // 基於本地項目的絕對路徑或者相對路徑,通過類 Unix 模式的路徑來描述
  • Cargo.toml 文件內容

    Cargo.toml 文件是 Rust 項目中用來配置和管理項目依賴、編譯選項、元數據等信息的配置文件。以下是 Cargo.toml 文件中常見的部分和它們的用途:

    1. [package]#

    這個部分定義了包的基本信息,包括名稱、版本、作者、描述等。

    [package]
    name = "my_project"         # 項目名稱
    version = "0.1.0"           # 項目版本
    edition = "2021"            # Rust 語言版本
    authors = ["Your Name <[email protected]>"] # 作者
    description = "A brief description of the project"  # 項目描述
    license = "MIT"             # 項目使用的許可證
    repository = "<https://github.com/yourusername/my_project>"  # 項目源碼倉庫
    
    

    2. [dependencies]#

    在這個部分中,定義了項目所依賴的庫及其版本。

    [dependencies]
    serde = "1.0"               # 添加 serde 庫作為依賴
    rand = { version = "0.8", features = ["std"] }  # 指定版本和附加特性
    
    

    3. [dev-dependencies]#

    這個部分用於定義開發環境中所需的依賴,這些依賴通常只在測試或開發工具中使用。

    [dev-dependencies]
    tokio = { version = "1", features = ["full"] }  # 僅用於開發階段
    
    

    4. [build-dependencies]#

    這個部分用於定義構建腳本 (build.rs) 中的依賴。

    [build-dependencies]
    cc = "1.0"                  # 用於構建原生代碼的庫
    
    

    5. [features]#

    用來定義項目的可選特性(功能)。這些特性可以讓用戶選擇性地啟用或禁用某些功能。

    [features]
    default = ["serde"]         # 默認啟用的特性
    extras = ["tokio"]          # 可選特性
    
    

    6. [package.metadata]#

    這個部分通常用於自定義工具的配置,它的內容和結構因工具而異。

    [package.metadata]
    docs = { rsdoc = true }     # 自定義文檔生成器的配置
    
    

    7. [workspace]#

    如果項目是一個工作空間(包含多個子項目),則使用這個部分定義工作空間的結構。

    [workspace]
    members = ["project1", "project2"]  # 工作空間中的成員項目
    
    

    8. [patch][replace]#

    用於替換或修補依賴項的版本,通常在處理依賴衝突或開發中使用。

    [patch.crates-io]
    serde = { git = "<https://github.com/serde-rs/serde>", branch = "master" }
    
    [replace]
    foo = { path = "../foo" }   # 替換依賴庫
    
    

    9. [profile]#

    這個部分允許你為不同的編譯配置(如 dev, release)自定義編譯選項。

    [profile.dev]
    opt-level = 1               # 在開發模式下使用的優化級別
    
    [profile.release]
    opt-level = 3               # 在發布模式下使用的優化級別
    
    

    10. [badges]#

    用於在文檔生成工具或其他服務上顯示狀態徽章(如構建狀態、代碼覆蓋率等)。

    [badges]
    travis-ci = { repository = "rust-lang/crates.io" }  # 顯示 Travis CI 徽章
    
    

    這是 Cargo.toml 文件中常見的部分及其用途。根據項目的複雜性和需求,文件內容可以更加豐富或簡單。

鏡像加速#

下載依賴太慢了? - Rust 語言聖經 (Rust Course)

[http]
check-revoke = false
# proxy = "http://x.x.x.x:8888" // 於虛擬機內未聯網開發時 作代理使用

[source.crates-io]
replace-with = 'rsproxy'
[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"
[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"
[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"

[net]
git-fetch-with-cli = true

泛型和特徵#

泛型就是一種多態。泛型主要目的是為程序員提供編程的便利,減少代碼的臃腫,同時極大地豐富語言本身的表達能力。

  • 枚舉
enum Option<T> {
		Some(T),
		None,
}
  • 結構體
struct Point<T> {
    x: T,
    y: T,
}
  • 方法
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };
    println!("p.x = {}", p.x());
}

泛型的性能#

在 Rust 中泛型是零成本的抽象,在使用泛型時,完全不用擔心性能上的問題,但是會損失編譯速度和增大了最終生成文件的大小。

Rust 通過在編譯時進行泛型代碼的 單態化(monomorphization) 來保證效率。單態化是一個通過填充編譯時使用的具體類型,將通用代碼轉換為特定代碼的過程。

編譯器所做的工作正好與我們創建泛型函數的步驟相反,編譯器尋找所有泛型代碼被調用的位置並針對具體類型生成代碼。

特徵的多重約束#

pub fn notify(item: &(impl Summary + Display)) {} // 語法糖形式
pub fn notify<T: Summary + Display>(item: &T) {} // 特徵約束的形式

Where 約束

當特徵約束變得很多時,函數的簽名將變得很複雜:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}

通過 where 對其做一些形式上的改進:

fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{}

動態數組#

https://doc.rust-lang.org/std/collections/

創建#


let mut v1 = Vec::with_capacity(10);
v1.extend([1, 2, 3]);    // 附加數據到 v
println!("Vector 長度是: {}, 容量是: {}", v.len(), v.capacity());

常用方法#

let mut v =  vec![1, 2];
assert!(!v.is_empty());         // 檢查 v 是否為空

v.reserve(100);                 // 調整 v 的容量,至少要有 100 的容量

v.insert(2, 3);                 // 在指定索引插入數據,索引值不能大於 v 的長度, v: [1, 2, 3] 
assert_eq!(v.remove(1), 2);     // 移除指定位置的元素並返回, v: [1, 3]
assert_eq!(v.pop(), Some(3));   // 刪除並返回 v 尾部的元素,v: [1]
assert_eq!(v.pop(), Some(1));   // v: []
assert_eq!(v.pop(), None);      // 記得 pop 方法返回的是 Option 枚舉值
v.clear();                      // 清空 v, v: []

let mut v1 = [11, 22].to_vec(); // append 操作會導致 v1 清空數據,增加可變聲明
v.append(&mut v1);              // 將 v1 中的所有元素附加到 v 中, v1: []
v.truncate(1);                  // 截斷到指定長度,多餘的元素被刪除, v: [11]
v.retain(|x| *x > 10);          // 保留滿足條件的元素,即刪除不滿足條件的元素

let mut v = vec![11, 22, 33, 44, 55];
// 刪除指定範圍的元素,同時獲取被刪除元素的迭代器, v: [11, 55], m: [22, 33, 44]
let mut m: Vec<_> = v.drain(1..=3).collect();    

let v2 = m.split_off(1);        // 指定索引處切分成兩個 vec, m: [22], v2: [33, 44]

let v2 = vec![11, 22, 33, 44, 55]; // 通過切片截取數組
let slice = &v2[1..=3];
assert_eq!(slice, &[22, 33, 44]);

排序#

fn main() {
    let mut vec = vec![1.0, 5.6, 10.3, 2.0, 15f32];    
    vec.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
    assert_eq!(vec, vec![1.0, 2.0, 5.6, 10.3, 15f32]);
}

結構體的排序

// 自定義一個按照年齡倒序排序的對比函數
people.sort_unstable_by(|a, b| b.age.cmp(&a.age));

排序需要實現 Ord 特性,如果把結構體實現了該特性,就不需要自定義對比函數了,但是,實現 Ord 需要我們實現 OrdEqPartialEqPartialOrd 這些屬性,可以 derive 這些屬性:

#[derive(Debug, Ord, Eq, PartialEq, PartialOrd)] // derive 屬性
struct Person {
    name: String,
    age: u32,
}

impl Person {
    fn new(name: String, age: u32) -> Person {
        Person { name, age }
    }
}

fn main() {
    let mut people = vec![
        Person::new("Zoe".to_string(), 25),
        Person::new("Al".to_string(), 60),
        Person::new("Al".to_string(), 30),
        Person::new("John".to_string(), 1),
        Person::new("John".to_string(), 25),
    ];

    people.sort_unstable();

    println!("{:?}", people);
}
// => [Person { name: "Al", age: 30 }, Person { name: "Al", age: 60 }, Person { name: "John", age: 1 }, Person { name: "John", age: 25 }, Person { name: "Zoe", age: 25 }]

derive 的默認實現會依據屬性的順序依次進行比較,如上述例子中,當 Personname 值相同,則會使用 age 進行比較。


HashMap#

創建#

fn main() {
	use std::collections::HashMap;
	
	let teams_list = vec![
	    ("中國隊".to_string(), 100),
	    ("美國隊".to_string(), 10),
	    ("日本隊".to_string(), 50),
	];
	
	let teams_map: HashMap<_,_> = teams_list.into_iter().collect();
	println!("{:?}",teams_map)
}

生命週期🌟#

懸垂指針#

生命週期的主要作用是避免懸垂引用,它會導致程序引用了本不該引用的數據

{
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("r: {}", r);
}

語法#

生命週期的語法以 ' 開頭,名稱往往是一個單獨的小寫字母,一般用 'a 來作為生命週期的名稱。 如果是引用類型的參數,那麼生命週期會位於引用符號 & 之後,並用一個空格來將生命週期引用參數分隔開:

&i32        // 一個引用
&'a i32     // 具有顯式生命週期的引用
&'a mut i32 // 具有顯式生命週期的可變引用

函數簽名

使用生命週期參數,需要先聲明 <'a>

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

靜態生命週期

在 Rust 中有一個非常特殊的生命週期,那就是 'static,擁有該生命週期的引用可以和整個程序活得一樣久。

在之前我們學過字符串字面量,提到過它是被硬編碼進 Rust 的二進制文件中,因此這些字符串變量全部具有 'static 的生命週期:

let s: &'static str = "我沒啥優點,就是活得久,嘿嘿";

生命週期消除#

在開始之前有幾點需要注意:

  • 消除規則不是萬能的,若編譯器不能確定某件事是正確時,會直接判為不正確,那麼你還是需要手動標註生命週期

  • 函數或者方法中,參數的生命週期被稱為 輸入生命週期,返回值的生命週期被稱為 輸出生命週期

  • 三條消除規則

    編譯器使用三條消除規則來確定哪些場景不需要顯式地去標註生命週期。其中第一條規則應用在輸入生命週期上,第二、三條應用在輸出生命週期上。若編譯器發現三條規則都不適用時,就會報錯,提示你需要手動標註生命週期。

    1. 每一個引用參數都會獲得獨自的生命週期

      例如一個引用參數的函數就有一個生命週期標註: fn foo<'a>(x: &'a i32),兩個引用參數的有兩個生命週期標註:fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此類推。

    2. 若只有一個輸入生命週期 (函數參數中只有一個引用類型),那麼該生命週期會被賦給所有的輸出生命週期,也就是所有返回值的生命週期都等於該輸入生命週期

      例如函數 fn foo(x: &i32) -> &i32x 參數的生命週期會被自動賦給返回值 &i32,因此該函數等同於 fn foo<'a>(x: &'a i32) -> &'a i32

    3. 若存在多個輸入生命週期,且其中一個是 &self&mut self,則 &self 的生命週期被賦給所有的輸出生命週期

      擁有 &self 形式的參數,說明該函數是一個 方法,該規則讓方法的使用便利度大幅提升。

一個複雜例子:泛型、特徵約束#

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>( // 聲明生命週期和範型
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann); // 因為要用格式化 {} 來輸出 ann,因此需要它實現 Display 特徵。
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Fn 特徵解決閉包生命週期#

fn main() {
   let closure_slision = fun(|x: &i32| -> &i32 { x });
   assert_eq!(*closure_slision(&45), 45);
   // Passed !
}

fn fun<T, F: Fn(&T) -> &T>(f: F) -> F {
   f
}

NLL (Non-Lexical Lifetime)#

之前我們在引用與借用那一章其實有講到過這個概念,簡單來說就是:引用的生命週期正常來說應該從借用開始一直持續到作用域結束,但是這種規則會讓多引用共存的情況變得更複雜:

fn main() {
   let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // 新編譯器中,r1,r2作用域在這裡結束

    let r3 = &mut s;
    println!("{}", r3);
}

按照上述規則,這段代碼將會報錯,因為 r1r2 的不可變引用將持續到 main 函數結束,而在此範圍內,我們又借用了 r3 的可變引用,這違反了借用的規則:要麼多個不可變借用,要麼一個可變借用

好在,該規則從 1.31 版本引入 NLL 後,就變成了:引用的生命週期從借用處開始,一直持續到最後一次使用的地方

按照最新的規則,我們再來分析一下上面的代碼。r1r2 不可變借用在 println! 後就不再使用,因此生命週期也隨之結束,那麼 r3可變借用就不再違反借用的規則。

Reborrow 再借用#

https://course.rs/advance/lifetime/advance.html#reborrow - 再借用

  • todo

錯誤處理#

Panic#

https://course.rs/basic/result-error/panic.html#panic - 原理剖析

backtrace 堆棧展開

如果我們想知道程序拋出 panic 之前經過了哪些調用環節,按照提示使用 RUST_BACKTRACE=1 cargo run$env:RUST_BACKTRACE=1 ; cargo run 來運行程序。

panic 時的兩種終止方式

當出現 panic! 時,程序提供了兩種方式來處理終止流程:堆棧展開直接終止

其中,默認的方式就是 堆棧展開,這意味著 Rust 會回溯堆棧上數據和函數調用,因此也意味著更多的善後工作,好處是可以給出充分的報錯信息和堆棧調用信息,便於事後的問題復盤。直接終止,顧名思義,不清理數據就直接退出程序,善後工作交與操作系統來負責。

對於絕大多數用戶,使用默認選擇是最好的,但是當你關心最終編譯出的二進制可執行文件大小時,那麼可以嘗試去使用直接終止的方式,例如下面的配置修改 Cargo.toml 文件,實現在 release 模式下遇到 panic 直接終止:

[profile.release]
panic = 'abort'

包和模塊#

引入項再導出#

當外部的模塊項 A 被引入到當前模塊中時 **,它的可見性自動被設置為私有的,** 如果你希望允許其它外部代碼引用我們的模塊項 A,那麼可以對它進行再導出:


mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

如上,使用 pub use 即可實現。這裡 use 代表引入 hosting 模塊到當前作用域,pub 表示將該引入的內容再度設置為可見。

當你希望將內部的實現細節隱藏起來或者按照某個目的組織代碼時,可以使用 pub use 再導出,例如統一使用一個模塊來提供對外的 API,那該模塊就可以引入其它模塊中的 API 然後進行再導出,最終對於用戶來說,所有的 API 都是由一個模塊統一提供的。


註釋和文檔#

代碼註釋#

// 行註釋

/**
	塊註釋
*/

文檔註釋#

/// `add_one` 將指定值加1
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}

/** `add_two` 將指定值加2

let arg = 5;
let answer = my_crate::add_two(arg);

assert_eq!(7, answer);

*/
pub fn add_one(x: i32) -> i32 {
    x + 1
}

常用文檔標題

在文檔註釋中除了 # Examples 這個標題,還有一些常用的,可以在項目中酌情使用:

  • Panics:函數可能會出現的異常狀況,這樣調用函數的人就可以提前規避
  • Errors:描述可能出現的錯誤及什麼情況會導致錯誤,有助於調用者針對不同的錯誤採取不同的處理方式
  • Safety:如果函數使用 unsafe 代碼,那麼調用者就需要注意一些使用條件,以確保 unsafe 代碼塊的正常工作

包和模塊級別的註釋#

也分為行註釋 //! 和塊註釋 /*! ... */

查看文檔#

cargo doc 

cargo doc --open

文檔測試#

https://course.rs/basic/comment.html# 文檔測試 doc-test

保留測試,隱藏文檔

在某些時候,我們希望保留文檔測試的功能,但是又要將某些測試用例的內容從文檔中隱藏起來:


/// ```
/// # // 使用#開頭的行會在文檔中被隱藏起來,但是依然會在文檔測試中運行
/// # fn try_main() -> Result<(), String> {
/// let res = world_hello::compute::try_div(10, 0)?;
/// # Ok(()) // returning from try_main
/// # }
/// # fn main() {
/// #    try_main().unwrap();
/// #
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Divide-by-zero"))
    } else {
        Ok(a / b)
    }
}

以上文檔註釋中,我們使用 # 將不想讓用戶看到的內容隱藏起來,但是又不影響測試用例的運行,最終用戶將只能看到那行沒有隱藏的 let res = world_hello::compute::try_div(10, 0)?;


格式化輸出#

https://course.rs/basic/formatted-output.html

{} 與#

與其它語言常用的 %d%s 不同,Rust 特立獨行地選擇了 {} 作為格式化佔位符,事實證明,這種選擇非常正確,它幫助用戶減少了很多使用成本,你無需再為特定的類型選擇特定的佔位符,統一用 {} 來替代即可,剩下的類型推導等細節只要交給 Rust 去做。

{} 類似,{:?} 也是佔位符:

  • {} 適用於實現了 std::fmt::Display 特徵的類型,用來以更優雅、更友好的方式格式化文本,例如展示給用戶
  • {:?} 適用於實現了 std::fmt::Debug 特徵的類型,用於調試場景

兩者的選擇很簡單,當你在寫代碼需要調試時,使用 {:?},剩下的場景,選擇 {}

**Display 特徵 **

與大部分類型實現了 Debug 不同,實現了 Display 特徵的 Rust 類型並沒有那麼多,往往需要我們自定義想要的格式化方式:


let i = 3.1415926;
let s = String::from("hello");
let v = vec![1, 2, 3];
let p = Person {
    name: "sunface".to_string(),
    age: 18,
};
println!("{}, {}, {}, {}", i, s, v, p);

運行後可以看到 vp 都無法通過編譯,因為沒有實現 Display 特徵,但是你又不能像派生 Debug 一般派生 Display,只能另尋他法:

  • 使用 {:?}{:#?}
  • 為自定義類型實現 Display 特徵
  • 使用 newtype 為外部類型實現 Display 特徵

下面來一一看看這三種方式。

{:#?}{:?} 幾乎一樣,唯一的區別在於它能更優美地輸出內容:


// {:?}
[1, 2, 3], Person { name: "sunface", age: 18 }

// {:#?}
[
    1,
    2,
    3,
], Person {
    name: "sunface",
}

因此對於 Display 不支持的類型,可以考慮使用 {:#?} 進行格式化,雖然理論上它更適合進行調試輸出。

** 為自定義類型實現 Display 特徵 **

如果你的類型是定義在當前作用域中的,那麼可以為其實現 Display 特徵,即可用於格式化輸出:


struct Person {
    name: String,
    age: u8,
}

use std::fmt;
impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "大佬在上,請受我一拜,小弟姓名{},年芳{},家裡無田又無車,生活苦哈哈",
            self.name, self.age
        )
    }
}
fn main() {
    let p = Person {
        name: "sunface".to_string(),
        age: 18,
    };
    println!("{}", p);
}
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。