《rust book2》讀書筆記

《rust book 2》中介紹了一些基礎的知識點,例如:引用, 借用, 泛型等等。另外,還有一些平時接觸較少,例如:智能指針,trait object,高級生命周期,marker trait (Sync, Send, Sized) , 函數指針 fn 和閉包等。這些特性在特殊場景很有用,同時熟悉這些也能讓我們更容易讀懂第三方庫源碼。

第 3 章 通用編程概念

數組 (array) 的數據是分配在棧上,而不是堆上;數組的大小在編譯時已確定,運行時不能改變。對于確定長度的集合適合用數組代替 vec,運行效率更高。

第 4 章 理解所有權

  1. 所有權的規則:
  • 每一個值會與一個變量綁定,該變量稱為 owner
  • 某一時刻只能有一個 owner
  • owner 超出作用域,與之綁定的值會被釋放

圖 4-1 為以下代碼片段關于所有權的內存模型:

let s1 = "hello".to_string();
let s2 = s1;
4-1 有權的內存模型
  1. 為確保不出現懸空引用,引用必須滿足下面兩個規則中的一個:
  • 只能有一個可變引用
  • 可以有多個不可變引用

圖 4-2 為以下代碼片段關于引用的內存模型:

let s1 = "hello".to_string();
let s = &s1;
4-2 引用的內存模型
  1. 切片能引用一個集合中一段連續的元素,圖 4-3 為以下代碼片段關于切片的內存模型:
 let s = "hello world".to_string();
 let world = &s[6..11];
4-3 切片的內存模型
  1. &String 與 &str 的區別,前者是引用整個字符串,后者是字符串中的連續字符;在函數參數中,建議使用 &str 作為參數類型,因為 &String 類型的變量會通過 deref 隱士轉換為 &str,反之則不行。

第 5 章 使用 struct 組織關聯的數據

  1. 使用 {:#?} 代替 {:?} 格式化,輸出值更可讀。

  2. 在對 struct 的方法調用過程中,rust 能自動引用和解引用,使得與方法的第一個參數匹配,這避免了繁瑣的顯示轉換。

struct Foo;
impl Foo {
        fn f(&self) {}
}
let foo = Foo;
foo.f();

在調用 foo 的 f 時,foo 會轉換成 &foo:
&foo.f();

第 7 章 使用 mod 復用和組織代碼

  1. 可見性規則:
  • 如果某一項為 public, 則在其父模塊中可訪問該項;
  • 如果為 private,則只能在當前模塊和子模塊中訪問;

第 8 章 集合

  1. 通過范圍索引訪問 String,如果范圍的邊界不在字符 (rust 中的字符是由 UTF-8 編碼。) 的邊界,會導致程序 panic。可以嘗試運行以下代碼:
let s = "你好";  
println!("{:?}", s.as_bytes());  
println!("{:?}", &s[0..2]);

如果需要遍歷字符串中的字符,需要使用第三方庫。

  1. 使用 entry 更新 HashMap 中的值:
let mut map: HashMap = HashMap::new();
let count = map.entry(&"hello".to_string()).or_insert(0);
*count += 1;
  1. 集合的初始化建議使用 with_capacity() 代替 new(),避免集合內存逐步增大過程中的內存拷貝。

第 10 章 泛型、特征、生命周期

  1. 泛型沒有運行時開銷,編譯器在編譯期會查找所有調用泛型的代碼,為泛型對應的具體類型生成代碼。例如:
fn f(i: T) {}
f(1_i32);

編譯器會生成:

 f_i32(i: i32) {}

需要注意的是,生命周期屬于一種泛型。

  1. 在函數或結構體定義中的生命周期,是為了表示多個引用的生命周期的相互關系,從而避免懸空指針。

  2. 生命周期的省略規則:

  • 函數輸入參數中的每個引用默認綁定一個生命周期,并且都不相同。例如:
 fn f(i: &str, j: &str) {} 和 fn f<'a, 'b>(i: &'a str, j: &'b str) 等價。
  • 如果輸入參數只有一個引用,那么輸出參數中所有引用的生命周期默認與輸入參數的引用相同。例如:
fn f(s: &str, i: i32) -> (&str, &str) 和 fn f<'a>(s: &'str, i: i32) -> (&'a str, &'a str) 等價。
  • 如果輸入參數有多個引用,但是有一個是 &self 或 &mut self,那么輸出參數中所有引用的生命周期與 self 的生命周期相同, 例如:
fn f(&self, i: &str) -> &str 和 fn f<'a, 'b>(&'a self, i: &'b str) -> &'a str 等價。

第 11 章 測試

  1. 通常,測試都寫在各自的 mod 中,測試每個函數的正確性,這種稱為單元測試。rust 還支持集成測試,這些測試放在與 src 平級的 test 目錄中,集成測試的目的是為了測試 crate 的公有 API 組合調用的正確性。

第 13 章 迭代器和閉包

  1. 每個函數和閉包的類型都不相同,即使兩個函數的輸入和輸出完全相同。函數可以隱士轉換為函數指針 fn, 閉包是實現 trait Fn, FnMut, FnOnce 之一的類型。

  2. 使用迭代器比 for 循環的效率會更高一點,熟練后可讀性和可維修性也比 for 要好,所以建議優先使用迭代器。

第 15 章 智能指針

  1. Box,是指向分配在堆上數據的指針,占用空間為 usize 的大小 (在 64 位機器上為 64 bytes,32 位機器上為 32 bytes)。使用場景:當類型的大小在編譯時無法確認,可以使用。例如:
enum List { Cons(i32, List), None }

由于 List 遞歸嵌套,編譯時會出錯,可以用 Box 改寫:

enum List { Cons(i32, Box), None }
  1. Rc,是引用計數指針,數據也分配在堆上,可以通過 clone() 將同一份數據和多個 owner 綁定,每 clone() 一次引用計數加 1,當引用計數為 0 時,會自動銷毀數據。需要注意的是,只能在單線程中使用。

  2. RefCell,是可以在運行時獲得數據的可變性指針,但是在運行時檢測可變性有性能開銷。同樣,也只能在單線程中使用。例如下面的代碼,能通過編譯,但是在運行時會 panic!。

use std::cell::RefCell;
let s = RefCell::new("hello".to_string());
let r1 = s.borrow_mut();
let r2 = s.borrow_mut();
  1. Rc 和 RefCell 聯合使用時,可能出現循環引用,會導致內存泄露。例如:
enum List {
    Cons(i32, RefCell>),
     Nil,
}

為了支持循環引用,同時避免內存泄露,可使用 downgrade 將 Rc 轉換成 Weak。在 Rc 上每次調用 clone() 時,會使得引用計數 strong_count 加 1;每次調用 downgrade 與之不同的是,weak_count 加 1,而 strong_count 不變。Rc 只要檢測到 strong_count 為 0,即使 weak_count 不為 0,也會釋放堆上的內存,從而避免了內存泄露。

15-1 使用 Weak 解決循環引用導致的內存泄露
struct Node {
    value: i32,
    pre: RefCell>,
    next: RefCell>,
}
  1. 智能指針通過解引用 * 操作符,能直接獲得數據。例如:
let s = "hello".to_string();
let j = Box::*new*(s.clone());
assert_eq!(s.clone(), *j);

use std::rc::Rc;
let j = Rc::*new*(s.clone());
assert_eq!(s.clone(), *j);

use std::cell::RefCell;
let j = RefCell::*new*(s.clone());
assert_eq!(s, *j.borrow());

能獲取數據的原因是,智能指針都實現了 Deref trait,將智能指針隱士轉換為數據的引用。assert_eq!(s.clone(), *j); 會轉換為:

assert_eq!(s.clone(), *(j.deref())); 其中,j.deref() 返回 &String。

第 16 章 并發

  1. 多線程之間通信時,優先使用 channel,其次才使用 Mutex。因為 channel 的接受方維護了一個數據隊列,發送方不會阻塞線程;而 Mutex 則可能由于數據競爭,阻塞線程,另外,也有可能產生死鎖。

  2. 上一章提到,在單線程中可以用 Rc,使得一份數據有多個 owner。在多線程中可以用 Arc,達到相同的效果,其中 A 表示原子性 (atomic)。

  3. 關于多線程間數據通信的兩個 marker trait: Send, Sync:

  • Send 表示數據的 owner 可以被轉移至其他線程,除了 Rc,其它原始類型都實現了 Send。
  • Sync 表示可以在多線程間通過引用訪問數據。即,類型 T 實現 Sync,和 &T 實現 Send 等價。Rc, RefCell 不是 Sync,Mutex 是 Sync。

第 17 章 trait object

  1. 對于一個 trait Draw,Box 是一個 trait object,表示 Box 里的類型都必須實現 Draw。通過 trait object,可以實現“多態”,在運行時動態分發。比較如下兩個結構體:
struct Screen1 {
  components: Vec<Box<Draw>>,
}

struct Screen2<T: Draw> {
  components: Vec<T>, 
}

Screen1 支持實現 Draw 的多種類型,在運行時調用不同類型的方法,有運行時開銷;Screen2 的單個實例只支持一種實現 Draw 類型的實例集合,在編譯時編譯器會生成調用類型的代碼(稱為 monomorphized),屬于靜態分發,沒有運行時開銷。

  1. trait object 需要 trait 是對象安全的,需要同時滿足以下兩個條件:
    *trait 不能和 Sized 綁定。
    Sized 也是一個 marker trait,表示在編譯時就能確定類型的大小,泛型參數會默認和 Sized 綁定;?Sized 表示類型可能是 Sized 也可能不是,triat 會默認和 ?Sized 綁定。這條規則可以這樣理解,trait object 在編譯時是無法確定堆上內存大小的,如果指定 trait 為 Sized,這兩者會相互矛盾。
  • trait 的所有方法需要是對象安全的。一個方法是對象安全的需要滿足下列規則之一:
    • 需要 self 為 Sized。
    • 同時滿足下列三個規則:
      • 不能有泛型參數。
      • 第一個參數必須為 self, &self 或 &mut self。
      • 除了第一個參數,其他參數不能為 self。

關于這些規則的解釋是,trait object 在編譯時會擦除 Self 的具體類型和泛型參數的類型,因此在運行時就無法推斷出這些參數的類型。

第 18 章 模式匹配

  1. 一些平時較少用到的語法:
  • if let {} else if {} else if let {} 可以組合使用
  • match 的一個分支可以一次匹配多個值:
let i = 1;
match i {
    1 | 2 => {}
    _ = {}
}
  • match 的分支對于數值和字符 (char) 類型支持范圍匹配:
let i = 1;
match i {
    1 ... 10 => {}
     _ => {}
}

match 的分支可以和條件判斷語句組合使用:

let i = 1;
let j = 10;
match i {
    1 if j <= 10 => {}
     _ => {}
}
  • 使用 .. 忽略結構體或 tuple 中不關心的部分:
let t = (1, 2, 3);
let (.., i) = t;
  • 使用模式匹配,會獲取數據的 ownership;如果只想或者某些情況下只能獲取引用,可以使用 ref 或 ref mut 進行匹配。

第 19 章 高級特性

  1. 生命周期的高級特性主要有以下三種:生命周期的子類型,生命周期和泛型綁定,trait object 的生命周期。
  • 生命周期的子類型表示一個引用的生命周期比另一個要長。例如:
fn foo<'a, 'b: 'a>(i: &'a str, j: &'b str) {}

其中的 'b:'a 表示 'b 的生命周期比 'a 要長,因此 'b 是 'a 的生命周期子類型。

  • 生命周期和泛型綁定表示泛型里如果有引用,那么一定比被綁定的生命周期要長。例如:
fn foo<'a, T: 'a>(i: &'a T);

其中的 T:'a 表示 T 中的引用的生命周期比 'a 要長。

  • trait object 的生命周期有以下幾條規則:

    • trait object 的默認生命周期是 'static。
      如果實現 trait 的結構體中有 &'a T 或者 &'a mut T,那么 trait object 默認的生命周期是 'a。例如以下代碼能編譯通過:
trait Foo {}
struct Bar<*'a*> {x: &*'a *i32}
let x = 1;
let bar = Bar {x: &x};
let foo = Box::*new*(bar);

需要注意的是,該場景只適用于在同一語句塊內,如果 trait object 作為函數的返回值,那么仍然需要顯示指定生命周期為 Box<Foo + 'a>。

  • 如果結構體中只有一個泛型和生命周期綁定 T: ‘a, 那么 trait object 默認的生命周期是 'a。
  • 如果有多個類似 T: 'a 的綁定,那么 trait object 需要顯示指定生命周期,語法為 Box<Foo + 'a>。
  1. 關聯類型是指將 trait 和一個類型占位符關聯,這樣 trait 中的方法的參數能使用該類型占位符。例如:
trait Iterator {
    type Item;
    fn next(&mut self) -> Option;
}

如果使用泛型實現,那么同一類型對同一 trait 能實現多次,關聯類型避免了這種情況的發生。

struct Foo {}
trait iterator {
    fn next(&mut self) -> Option
}
impl iterator for Foo { fn next(&mut self) -> int {}}
impl iterator for Foo { fn next(&mut self) -> String {}}
  1. 如果 struct 中的方法和 trait 中的方法重名,那么需要使用下面的方法調用:
trait Foo { fn f(&self); }
struct Bar;
impl Bar {
    fn f(&self, i: i32) {
        println!("i32");
    }
}

impl Foo for Bar {
    fn f(&self) {
        println!("trait")
    }
}

 let b = Bar;
 Foo::f(&b);
  1. 動態大小類型,是指所占的內存只有在運行時才能確定。str 就是動態類型,所以不能直接使用 str,必須要引用 &str。回顧圖 4-3,&str 有兩個值,一個是指針,另一個是所指數據的長度。另外,trait 也是動態大小類型,所以只能使用 &Trait 或 Box。

  2. 函數能隱士轉換為函數指針類型 fn,注意與閉包 Fn trait 的區別。fn 已經實現了 Fn, FnMut, FnOnce,所以一個函數的參數為閉包,可以將一個函數指針傳入。

總結

總體而言,《rust book 2》對 rust 的特性介紹得比較全面,除了宏還沒有,深入淺出,讀起來比較流暢。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。