《rust book 2》中介紹了一些基礎的知識點,例如:引用, 借用, 泛型等等。另外,還有一些平時接觸較少,例如:智能指針,trait object,高級生命周期,marker trait (Sync, Send, Sized) , 函數指針 fn 和閉包等。這些特性在特殊場景很有用,同時熟悉這些也能讓我們更容易讀懂第三方庫源碼。
第 3 章 通用編程概念
數組 (array) 的數據是分配在棧上,而不是堆上;數組的大小在編譯時已確定,運行時不能改變。對于確定長度的集合適合用數組代替 vec,運行效率更高。
第 4 章 理解所有權
- 所有權的規則:
- 每一個值會與一個變量綁定,該變量稱為 owner
- 某一時刻只能有一個 owner
- owner 超出作用域,與之綁定的值會被釋放
圖 4-1 為以下代碼片段關于所有權的內存模型:
let s1 = "hello".to_string();
let s2 = s1;
- 為確保不出現懸空引用,引用必須滿足下面兩個規則中的一個:
- 只能有一個可變引用
- 可以有多個不可變引用
圖 4-2 為以下代碼片段關于引用的內存模型:
let s1 = "hello".to_string();
let s = &s1;
- 切片能引用一個集合中一段連續的元素,圖 4-3 為以下代碼片段關于切片的內存模型:
let s = "hello world".to_string();
let world = &s[6..11];
- &String 與 &str 的區別,前者是引用整個字符串,后者是字符串中的連續字符;在函數參數中,建議使用 &str 作為參數類型,因為 &String 類型的變量會通過 deref 隱士轉換為 &str,反之則不行。
第 5 章 使用 struct 組織關聯的數據
使用 {:#?} 代替 {:?} 格式化,輸出值更可讀。
在對 struct 的方法調用過程中,rust 能自動引用和解引用,使得與方法的第一個參數匹配,這避免了繁瑣的顯示轉換。
struct Foo;
impl Foo {
fn f(&self) {}
}
let foo = Foo;
foo.f();
在調用 foo 的 f 時,foo 會轉換成 &foo:
&foo.f();
第 7 章 使用 mod 復用和組織代碼
- 可見性規則:
- 如果某一項為 public, 則在其父模塊中可訪問該項;
- 如果為 private,則只能在當前模塊和子模塊中訪問;
第 8 章 集合
- 通過范圍索引訪問 String,如果范圍的邊界不在字符 (rust 中的字符是由 UTF-8 編碼。) 的邊界,會導致程序 panic。可以嘗試運行以下代碼:
let s = "你好";
println!("{:?}", s.as_bytes());
println!("{:?}", &s[0..2]);
如果需要遍歷字符串中的字符,需要使用第三方庫。
- 使用 entry 更新 HashMap 中的值:
let mut map: HashMap = HashMap::new();
let count = map.entry(&"hello".to_string()).or_insert(0);
*count += 1;
- 集合的初始化建議使用 with_capacity() 代替 new(),避免集合內存逐步增大過程中的內存拷貝。
第 10 章 泛型、特征、生命周期
- 泛型沒有運行時開銷,編譯器在編譯期會查找所有調用泛型的代碼,為泛型對應的具體類型生成代碼。例如:
fn f(i: T) {}
f(1_i32);
編譯器會生成:
f_i32(i: i32) {}
需要注意的是,生命周期屬于一種泛型。
在函數或結構體定義中的生命周期,是為了表示多個引用的生命周期的相互關系,從而避免懸空指針。
生命周期的省略規則:
- 函數輸入參數中的每個引用默認綁定一個生命周期,并且都不相同。例如:
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 章 測試
- 通常,測試都寫在各自的 mod 中,測試每個函數的正確性,這種稱為單元測試。rust 還支持集成測試,這些測試放在與 src 平級的 test 目錄中,集成測試的目的是為了測試 crate 的公有 API 組合調用的正確性。
第 13 章 迭代器和閉包
每個函數和閉包的類型都不相同,即使兩個函數的輸入和輸出完全相同。函數可以隱士轉換為函數指針 fn, 閉包是實現 trait Fn, FnMut, FnOnce 之一的類型。
使用迭代器比 for 循環的效率會更高一點,熟練后可讀性和可維修性也比 for 要好,所以建議優先使用迭代器。
第 15 章 智能指針
- Box,是指向分配在堆上數據的指針,占用空間為 usize 的大小 (在 64 位機器上為 64 bytes,32 位機器上為 32 bytes)。使用場景:當類型的大小在編譯時無法確認,可以使用。例如:
enum List { Cons(i32, List), None }
由于 List 遞歸嵌套,編譯時會出錯,可以用 Box 改寫:
enum List { Cons(i32, Box), None }
Rc,是引用計數指針,數據也分配在堆上,可以通過 clone() 將同一份數據和多個 owner 綁定,每 clone() 一次引用計數加 1,當引用計數為 0 時,會自動銷毀數據。需要注意的是,只能在單線程中使用。
RefCell,是可以在運行時獲得數據的可變性指針,但是在運行時檢測可變性有性能開銷。同樣,也只能在單線程中使用。例如下面的代碼,能通過編譯,但是在運行時會 panic!。
use std::cell::RefCell;
let s = RefCell::new("hello".to_string());
let r1 = s.borrow_mut();
let r2 = s.borrow_mut();
- 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,也會釋放堆上的內存,從而避免了內存泄露。
struct Node {
value: i32,
pre: RefCell>,
next: RefCell>,
}
- 智能指針通過解引用 * 操作符,能直接獲得數據。例如:
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 章 并發
多線程之間通信時,優先使用 channel,其次才使用 Mutex。因為 channel 的接受方維護了一個數據隊列,發送方不會阻塞線程;而 Mutex 則可能由于數據競爭,阻塞線程,另外,也有可能產生死鎖。
上一章提到,在單線程中可以用 Rc,使得一份數據有多個 owner。在多線程中可以用 Arc,達到相同的效果,其中 A 表示原子性 (atomic)。
關于多線程間數據通信的兩個 marker trait: Send, Sync:
- Send 表示數據的 owner 可以被轉移至其他線程,除了 Rc,其它原始類型都實現了 Send。
- Sync 表示可以在多線程間通過引用訪問數據。即,類型 T 實現 Sync,和 &T 實現 Send 等價。Rc, RefCell 不是 Sync,Mutex 是 Sync。
第 17 章 trait object
- 對于一個 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),屬于靜態分發,沒有運行時開銷。
- 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 章 模式匹配
- 一些平時較少用到的語法:
- 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 章 高級特性
- 生命周期的高級特性主要有以下三種:生命周期的子類型,生命周期和泛型綁定,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 object 的默認生命周期是 'static。
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>。
- 關聯類型是指將 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 {}}
- 如果 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);
動態大小類型,是指所占的內存只有在運行時才能確定。str 就是動態類型,所以不能直接使用 str,必須要引用 &str。回顧圖 4-3,&str 有兩個值,一個是指針,另一個是所指數據的長度。另外,trait 也是動態大小類型,所以只能使用 &Trait 或 Box。
函數能隱士轉換為函數指針類型 fn,注意與閉包 Fn trait 的區別。fn 已經實現了 Fn, FnMut, FnOnce,所以一個函數的參數為閉包,可以將一個函數指針傳入。
總結
總體而言,《rust book 2》對 rust 的特性介紹得比較全面,除了宏還沒有,深入淺出,讀起來比較流暢。