詳解rust?自動(dòng)化測(cè)試、迭代器與閉包、智能指針、無畏并發(fā)
編寫測(cè)試可以讓我們的代碼在后續(xù)迭代過程中不出現(xiàn)功能性缺陷問題;理解迭代器、閉包的函數(shù)式編程特性;Box<T>
智能指針在堆上存儲(chǔ)數(shù)據(jù),Rc<T>
智能指針開啟多所有權(quán)模式等;理解并發(fā),如何安全的使用線程,共享數(shù)據(jù)。
自動(dòng)化測(cè)試
編寫測(cè)試以方便我們?cè)诤罄m(xù)的迭代過程中,不會(huì)改壞代碼。保證了程序的健壯性。
測(cè)試函數(shù)通常進(jìn)行如下操作:
- 設(shè)置需要的數(shù)據(jù)或狀態(tài)
- 運(yùn)行需要測(cè)試的代碼
- 斷言其結(jié)果是我們期望的
在 rust 中,通過test
屬性、斷言宏和一些屬性設(shè)置來測(cè)試代碼。
$> cargo new ifun-grep --lib
創(chuàng)建項(xiàng)目時(shí),通過--lib
表明創(chuàng)建一個(gè)庫(kù),會(huì)默認(rèn)生成一個(gè)測(cè)試示例,在src/lib.rs
中
pub fn add(left: usize, right: usize) -> usize { left + right } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { let result = add(2, 2); assert_eq!(result, 4); } }
進(jìn)入到項(xiàng)目中,執(zhí)行cargo test
就會(huì)看到執(zhí)行完測(cè)試的詳細(xì)信息。包括了測(cè)試數(shù)量、通過測(cè)試數(shù)、失敗測(cè)試數(shù)等等維度
首先使用mod tests
定義了一個(gè) tests 模塊,內(nèi)部函數(shù)需要使用外部方法,在最頂部調(diào)用了use super::*;
。這在包的一節(jié)里已有說明。
#[cfg(test)]
標(biāo)注測(cè)試模塊。它可以告訴 rust 在編譯時(shí)不需要包含該測(cè)試代碼。
#[test]
表明是測(cè)試函數(shù),通過 assert_eq!()
斷言結(jié)果值是否相同。
可以手動(dòng)改動(dòng)一下斷言值assert_eq!(result, 5)
,再次執(zhí)行可以看到測(cè)試不通過,并給出了結(jié)果的不同之處。
由 rust 標(biāo)準(zhǔn)庫(kù)提供的斷言測(cè)試宏,幫助我們處理結(jié)果值。結(jié)果與預(yù)期相同時(shí),則測(cè)試會(huì)通過;不一樣時(shí),則會(huì)調(diào)用panic!
宏,導(dǎo)致測(cè)試失敗。
assert!()
一個(gè)必傳參數(shù),true
是測(cè)試通過;false
測(cè)試失敗。assert_eq!()
兩個(gè)必傳參數(shù),比對(duì)它們是否相同。assert_ne!
兩個(gè)必傳參數(shù),比對(duì)它們是否不相同。
assert_eq!
和assert_ne
斷言失敗時(shí),會(huì)打印出兩個(gè)值,便于觀察為什么失敗。因?yàn)闀?huì)打印輸出,所以兩個(gè)值必須實(shí)現(xiàn)PartialEq
和Debug trait
可以被比較和輸出調(diào)試。
如果是我們自定義的結(jié)構(gòu)體或枚舉類型,則可以直接增加#[derive(PartialEq, Debug)]
注解。如果是復(fù)雜的類型,則需要派生宏trait
,這在后面的文章會(huì)講。
#[derive(PartialEq,Debug)] struct User { name: String, }
宏除了它們必須的參數(shù)之外,也可以傳遞更多的參數(shù),這些參數(shù)會(huì)被傳遞給format!()
打印輸出。這樣我們可以增加一些輸出,方便解決斷言失敗的問題
assert_eq!(result, 5, "hello rust!");
測(cè)試程序處理錯(cuò)誤
除了測(cè)試程序正常執(zhí)行邏輯的結(jié)果,也需要測(cè)試程序發(fā)生錯(cuò)誤時(shí),是否按照我們的錯(cuò)誤處理邏輯 處理了錯(cuò)誤。
假設(shè)我們的被測(cè)試函數(shù)接受的參數(shù)不能大于100
,大于時(shí)panic
錯(cuò)誤 信息
pub fn add(left: usize, right: usize) -> usize { if right > 100 { panic!("the value exceeds 100!"); } left + right } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { let result = add(2, 102); assert_eq!(result, 104); } }
執(zhí)行測(cè)試cargo test
,就算斷言結(jié)果時(shí)邏輯正確的,但是我們的函數(shù)限制了參數(shù)最大值,測(cè)試不通過。
增加測(cè)試用例來測(cè)試這種場(chǎng)景,通過增加#[should_panic]
來處理程序確實(shí)有這種限制,并panic!
。
#[test] #[should_panic] fn value_exceed_100() { add(5, 120); }
執(zhí)行cargo test
,可以看到測(cè)試示例通過了。如果我們符合參數(shù)要求,測(cè)試示例就會(huì)是失敗
但如果我們代碼中有多個(gè)錯(cuò)誤panic!()
,就會(huì)有同樣的多個(gè)測(cè)試示例不通過,打印輸出并沒有給我們足夠的信息去找到問題所在。
通過should_panic
可選擇參數(shù)expected
提供一個(gè)錯(cuò)誤描述信息,
pub fn add(left: usize, right: usize) -> usize { if right > 100 { panic!("the value exceeds 100!,got {}", right) } else if right < 50 { panic!("the value does not less than 50!,got {}", right) } left + right } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic(expected = "exceeds 100!")] fn value_exceed_100() { add(5, 99); } #[test] #[should_panic(expected = "less than 50!")] fn value_not_less_50() { add(59, 59); } }
也可以通過Result<T,E>
編寫測(cè)試,在程序失敗時(shí),返回Err
而不是panic
;
#[test] fn add_equal() -> Result<(), String> { if add(5, 105) == 111 { Ok(()) } else { Err(String::from("Error in add")) } }
此時(shí)不能使用#[should_panic()]
注解。也不能使用表達(dá)式?
控制測(cè)試運(yùn)行
cargo test
在測(cè)試模式下編譯代碼并發(fā)運(yùn)行生成的測(cè)試二進(jìn)制文件。
1.可以通過設(shè)置測(cè)試線程,單次只執(zhí)行一個(gè)測(cè)試示例
$> cargo test -- --test-threads=1
測(cè)試線程為 1,程序不會(huì)使用任何并行機(jī)制。
2.默認(rèn)的測(cè)試在測(cè)試示例通過時(shí),不會(huì)打印輸出。通過設(shè)置在測(cè)試成功時(shí)也輸出程序中的打印
$> cargo test -- --show-output
3.默認(rèn)的cargo test
會(huì)運(yùn)行所有測(cè)試,通過指定名稱來運(yùn)行部分測(cè)試
$> cargot test add_equal
過濾運(yùn)行多個(gè)測(cè)試,可以通過指定測(cè)試名稱的一部分,只要匹配這個(gè)名稱的測(cè)試都會(huì)被運(yùn)行。
$> cargot test value
通過#[ignore]
標(biāo)記忽略該測(cè)試。
#[test] #[ignore] fn add_equal() -> Result<(), String> { if add(5, 105) == 110 { Ok(()) } else { Err(String::from("Error in add")) } }
測(cè)試被忽略,但是可以通過cargot test -- --ignored
來運(yùn)行被忽略的測(cè)試。
如果想運(yùn)行所有的測(cè)試,可以通過cargot test -- --include-ignored
集成測(cè)試
單元測(cè)試可以在指定的模塊中書寫測(cè)試實(shí)例,每次測(cè)試一個(gè)模塊,也可以測(cè)試私有接口。
集成測(cè)試對(duì)庫(kù)來說是外部的,只能測(cè)試公有接口,可測(cè)試多個(gè)模塊。通過創(chuàng)建tests
目錄編寫?yīng)毩⒌臏y(cè)試文件。
tests/lib.rs
use ifun_grep; #[test] #[should_panic(expected = "exceeds")] fn value_exceed_100() { ifun_grep::add(5, 99); }
隨著集成測(cè)試模塊的增多,我們需要更好的組織它們,可以根據(jù)測(cè)試的功能將測(cè)試分組。將一些測(cè)試公共模塊抽離出來,作為其他測(cè)試功能組的測(cè)試函數(shù)調(diào)用
比如tests/common.rs
pub fn init(){ // something init }
再執(zhí)行cargo test
,會(huì)看到運(yùn)行了tests/common.rs
運(yùn)行了 0 個(gè)測(cè)試。這顯然是我們不需要的,可以改寫文件目錄tests/common/mod.rs
,這會(huì)告訴 rust 不要將common
看作一個(gè)集成測(cè)試文件。
迭代器與閉包
rust 類似函數(shù)式編程語言的特性。可以將函數(shù)作為參數(shù)值或返回值、將函數(shù)賦值給變量等。
閉包
可以儲(chǔ)存在變量里的類似函數(shù)的結(jié)構(gòu)。保存在一個(gè)變量中或作為參數(shù)傳遞給其他函數(shù)的匿名函數(shù)。
閉包允許捕獲被定義時(shí)所在作用域中的值。
#[derive(Debug)] enum Name { Admin, Test, } #[derive(Debug)] struct User {} impl User { fn get_name(&self, name: Option<Name>) -> Name { name.unwrap_or_else(|| self.random_name()) } fn random_name(&self) -> Name { Name::Admin } } fn main(){ let user = User {}; println!("{:?}", user.get_name(Some(Name::Test))); println!("{:?}", user.get_name(None)); }
unwrap_or_else
方法接受一個(gè)閉包函數(shù),當(dāng)一個(gè)Some
值存在時(shí)直接返回,如果不存在則執(zhí)行其傳入的閉包函數(shù)計(jì)算一個(gè)值返回。
閉包不需要在參數(shù)或返回值上注明類型。閉包通常只關(guān)聯(lián)小范圍的上下文而非任意情景,所以編譯器可以推導(dǎo)出參數(shù)和返回值類型。
也可以顯示定義閉包的參數(shù)和返回值的類型:
fn main(){ let get_age = |age: i8| -> i8 { age }; // let get_age = |age| age; println!("{}", get_age(32)); }
相對(duì)于增加參數(shù)或返回值類型使得書寫更加的繁瑣。而對(duì)于未標(biāo)注類型的閉包,在第一次調(diào)用后就確定其參數(shù)和返回值類型,再傳其他類型時(shí)就會(huì)報(bào)錯(cuò)。
fn main(){ let get_age = |age| age; println!("{}", get_age(String::from("admin"))); // 調(diào)用出錯(cuò),已經(jīng)確定了參數(shù)和返回值類型為String println!("{}", get_age(32)); }
捕獲引用或移動(dòng)所有權(quán)
在傳遞給閉包參數(shù)時(shí),需要考慮參數(shù)的傳遞方式:不可變借用、可變借用和獲取所有權(quán)。這是根據(jù)傳遞的值決定的。
對(duì)于不可變借用,變量可以在任何情形下被訪問。
let str = String::from("hboot"); let print_str = || println!("{:?}", str); println!("{str}"); print_str(); println!("{str}");
而對(duì)于可變借用,則只能在借用結(jié)束后調(diào)用.聲明的閉包函數(shù)也需要mut
聲明
let mut str = String::from("hboot"); let mut print_str = || str.push_str("-rust"); // println!("{str}"); print_str(); println!("{str}");
通過move
關(guān)鍵字將變量的所有權(quán)轉(zhuǎn)移閉包所在的環(huán)境中。
use std::thread; fn main(){ let mut str = String::from("hboot"); println!("{str}"); thread::spawn(move || { str.push_str("-rust"); println!("{str}") }) .join() .unwrap(); }
此時(shí),將變量str
值的所有權(quán)轉(zhuǎn)移到了新線程中,主線程則不能再使用。
將被捕獲的值移出閉包和 Fn trait
在閉包環(huán)境中,捕獲和處理值的方式會(huì)影響閉包 trait 的實(shí)現(xiàn)。trait 是函數(shù)或結(jié)構(gòu)體指定它們可以使用什么類型的閉包。
從閉包如何任何處理值、閉包自動(dòng)、漸進(jìn)實(shí)現(xiàn)一個(gè)、多個(gè) Fn
trait
FnOnce
適用于調(diào)用一次的閉包。所有閉包都是實(shí)現(xiàn)這個(gè) trait,它會(huì)將捕獲的值移除閉包。FnMut
不會(huì)將捕獲的值移除閉包,可能會(huì)修改值。會(huì)被調(diào)用 多次。Fn
不會(huì)移除捕獲的值,也不修改捕獲的值。會(huì)被調(diào)用多次而不改變其環(huán)境。
這是Option<T>
的unwrap_or_else()
方法定義
impl<T> Option<T> { pub fn unwrap_or_else<F>(self, f: F) -> T where F: FnOnce() -> T { match self { Some(x) => x, None => f(), } } }
F
就是閉包指定的類型,T
是返回值類型。FnOnce()->T
表明了閉包會(huì)被調(diào)用一次,有值時(shí)Some
,返回值;沒有值時(shí)None
,f
調(diào)用一次。
在使用閉包時(shí),如果我們不需要捕獲其環(huán)境中的值,則可以不使用閉包,而使用傳遞函數(shù)作為參數(shù)。
迭代器
迭代器是處理元素序列的方式。遍歷序列中的每一項(xiàng)以及決定序列何時(shí)結(jié)束的邏輯。
fn main(){ let arr = [1, 2, 3, 4]; for val in arr { println!("{}", val) } }
迭代器都定義了Iterator
trait,并實(shí)現(xiàn)next
方法。調(diào)用next
返回迭代器的一個(gè)項(xiàng),封裝在Some
中,結(jié)束后返回None
pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }
type Item
和Self::Item
定義了 trait 的關(guān)聯(lián)類型。表明了迭代器返回值類型為Item
可以通過next()
方法迭代獲取值:
fn main(){ let arr = [1, 2, 3, 4]; let mut iter = arr.iter(); println!("{:?}", iter.next()); println!("{:?}", iter.next()); }
iter()
生成一個(gè)不可變引用的迭代器。對(duì)于迭代器實(shí)例iter
必須是mut
可變的。
into_ter()
獲取到 arr 所有權(quán)的迭代器。iter_mut()
可以獲取到可變引用迭代器。 消費(fèi)適配器
調(diào)用next()
方法的方法被稱為消費(fèi)適配器。
fn main() { let arr = [1, 2, 3, 4]; let total: i8 = arr.iter().sum(); println!("{}", total); }
這些方法總是會(huì)獲取迭代器的所有權(quán)并反復(fù)調(diào)用 next
來遍歷迭代器。sum()
方法返回調(diào)用next
方法獲取值,最終返回和值。
迭代器適配器
將當(dāng)前迭代器變?yōu)椴煌愋偷牡鳌?梢枣準(zhǔn)秸{(diào)用多個(gè)迭代器適配器,但是每次調(diào)用都必須調(diào)用消費(fèi)適配器來獲取調(diào)用結(jié)果。
fn main(){ let arr = [1, 2, 3, 4]; let arr2: Vec<_> = arr.iter().map(|val| val + 1).collect(); for val in arr2 { println!("{}", val) } }
map()
方法接受一個(gè)閉包函數(shù),可以在遍歷元素上執(zhí)行任何操作。進(jìn)行了一次迭代適配器操作,然后通過collect()
方法獲取調(diào)用的結(jié)果值。
智能指針
指針是一個(gè)包含內(nèi)存地址的變量。智能指針是一類數(shù)據(jù)結(jié)構(gòu),表現(xiàn)同指針,并擁有額外的元數(shù)據(jù)和功能。
智能指針通常使用結(jié)構(gòu)體實(shí)現(xiàn),實(shí)現(xiàn)了Deref
和Drop
trait。deref trait
允許智能指針結(jié)構(gòu)體實(shí)例表現(xiàn)的像引用一樣;drop trait
允許智能指針離開作用域時(shí)自定義運(yùn)行代碼
標(biāo)準(zhǔn)庫(kù)中常用的智能指針:
Box<T>
用于在堆上分配值Rc<T>
引用計(jì)數(shù)類型,其數(shù)據(jù)可以有多個(gè)所有者Ref<T>、RefMut<T>
通過RefCell<T>
訪問,這是一個(gè)在運(yùn)行時(shí)執(zhí)行借用規(guī)則的類型。
Box<T>
智能指針 box 允許將一個(gè)值放在堆上而不是棧上。留在棧上的則是指向堆數(shù)據(jù)的指針。
在以下情況下可以考慮使用:
- 編譯時(shí)未知大小的類型,又想在確切大小的上下文中使用這個(gè)類型的值。
- 當(dāng)有大量數(shù)據(jù)不被拷貝的情況下轉(zhuǎn)移所有權(quán)的時(shí)候
- 當(dāng)有一個(gè)值只關(guān)心它的類型是否實(shí)現(xiàn)特定 trait,而不是具體類型的時(shí)候
fn main(){ let b = Box::new(100); println!("{}", b); }
直接聲明創(chuàng)建 box 類型變量,并分配了一個(gè)值100
存儲(chǔ)在堆上, 可以直接訪問變量訪問值。
通過cons
list 數(shù)據(jù)結(jié)構(gòu)定義遞歸數(shù)據(jù)類型
它是construct function
的縮寫,利用兩個(gè)參數(shù)構(gòu)造一個(gè)新的列表.最后一項(xiàng)值包含了Nil
值,標(biāo)識(shí)結(jié)束
enum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main(){ let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); }
Cons
可能會(huì)無限嵌套下去,為了保證 rust 編譯時(shí)計(jì)算需要的大小,只能通過Box
來幫助 rust 計(jì)算出List
需要的大小。
Deref
trait 重載解引用運(yùn)算符*
之前已經(jīng)使用過*
解引用值,可以獲取到指針指向引用的值。
fn main(){ let mut s = String::from("hboot"); let s1 = &mut s; *s1 += " admin"; println!("{}", s) }
s1
是 s 的可變引用,再通過*
解引用后,可以修改存儲(chǔ)在堆上的數(shù)據(jù)。
也可以通過Box<T>
代替引用,和*
擁有相同的功能。
fn main(){ let s = String::from("hboot"); let mut s1 = Box::new(s); *s1 += " admin"; println!("{:?}", s1); }
Box
會(huì)拷貝s
在棧上的指針數(shù)據(jù),導(dǎo)致存儲(chǔ)在堆上的數(shù)據(jù)所有權(quán)被轉(zhuǎn)移,s
在后續(xù)變的不可用。
自定義實(shí)現(xiàn)一個(gè)智能指針MyBox
,它可以做到上面的解引用操作
#[derive(Debug)] struct MyBox<T>(T); impl<T> MyBox<T> { fn new(val: T) -> MyBox<T> { MyBox(val) } }
實(shí)現(xiàn)了一個(gè)元組結(jié)構(gòu)體,自定義實(shí)例new
方法,接受一個(gè)參數(shù)進(jìn)行初始化操作。還需要實(shí)現(xiàn)解引用功能,Deref
trait 由標(biāo)準(zhǔn)庫(kù)提供,實(shí)現(xiàn) deref 方法
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } }
上述的解引用例子,則可以由MyBox
代替實(shí)現(xiàn)。type Target = T
定義了 trait 的關(guān)聯(lián)類型,&self.0
訪問元組結(jié)構(gòu)體的第一個(gè)元素。
fn main(){ let s = String::from("hboot"); let s1 = MyBox::new(s); // *s1 += " admin"; println!("{:?}", *s1); }
因?yàn)閷?shí)現(xiàn)的是Deref
所以不能修改,修改時(shí)需要實(shí)現(xiàn)DerefMut
trait。
實(shí)現(xiàn)了Deref
trait 的數(shù)據(jù)類型,在函數(shù)傳參時(shí),可做到隱式轉(zhuǎn)換,而不需要手動(dòng)去轉(zhuǎn)換為參數(shù)需要的類型。
fn print(val: &str) { println!("{}", val) } fn main(){ // 輸出上面的示例 s1 print(&s1); }
對(duì)于數(shù)據(jù)的強(qiáng)制轉(zhuǎn)換,只能將可變引用轉(zhuǎn)為不可變引用;不能將不可變引用轉(zhuǎn)為可變引用。
Drop
trait 運(yùn)行清理代碼
實(shí)現(xiàn)了Drop
trait 的數(shù)據(jù),在離開作用域時(shí),會(huì)調(diào)用其實(shí)現(xiàn)的drop
方法,它獲取一個(gè)可變引用。
為上述的MyBox
實(shí)現(xiàn)Drop
,無需引入,Drop
trait 是 prelude 的。
impl<T> Drop for MyBox<T> { fn drop(&mut self) { println!("mybox drop value"); } }
再次調(diào)用執(zhí)行,可以看到最終在程序執(zhí)行完畢后,打印輸出了mybox drop value
. drop
會(huì)自動(dòng)執(zhí)行,而無需手動(dòng)調(diào)用。
如果想要提前銷毀資源,則需要std::mem::drop
,可以調(diào)用drop
方法
fn main(){ drop(s1); // 手動(dòng)清理后,后續(xù)不能再使用s1 // print(&s1); }
Rc<T>
引用計(jì)數(shù)啟用多所有權(quán)模式
在圖形結(jié)構(gòu)中,每個(gè)節(jié)點(diǎn)都有多個(gè)邊指向,所以每個(gè)節(jié)點(diǎn)都會(huì)擁有指向它的邊的所有權(quán)。
通過使用Rc<T>
類型,記錄被引用的數(shù)量,來確定這個(gè)值有沒有被引用。如果為 0 沒有被引用,則會(huì)被清理。
Rc<T>
只適用于單線程
創(chuàng)建Rc
類型的變量s
,然后通過Rc::clone
克隆變量s
生成s1\s2
.
use std::rc::Rc; fn main(){ let s = Rc::new(String::from("hboot")); let s1 = Rc::clone(&s); let s2 = Rc::clone(&s); println!("s:{},s1:{},s2:{}", s, s1, s2) }
這里可以看到s1、s2
沒有獲取s
的所有權(quán),它們?nèi)匀煌瑫r(shí)生效。Rc::clone
不同于深拷貝,只會(huì)增加引用計(jì)數(shù)。
可以通過strong_count()
方法查看被引用次數(shù)
fn main(){ let s = Rc::new(String::from("hboot")); println!("s create - {}", Rc::strong_count(&s)); let s1 = Rc::clone(&s); println!("s1 create - {}", Rc::strong_count(&s)); { let s2 = Rc::clone(&s); println!("s2 create - {}", Rc::strong_count(&s)); } println!("s2 goes out of scope - {}", Rc::strong_count(&s)); }
執(zhí)行測(cè)試輸出為
通過不可變引用,Rc<T>
允許程序在多個(gè)部分之間只讀地共享數(shù)據(jù)。
RefCell<T>
允許修改不可變引用
根據(jù) rust 的不可變引用規(guī)則,被引用的變量是不允許修改。但是在某些模式下,可以做到修改,也就是內(nèi)部可變性模式。
內(nèi)部可變性通過在數(shù)據(jù)結(jié)構(gòu)中使用unsafe
代碼來模糊 rust 的不可變性和借用規(guī)則。unsafe
不安全代碼表明我們需要手動(dòng)去檢查代碼而不是讓編譯器檢查。
RefCell<T>
類型是在代碼運(yùn)行時(shí)作用檢測(cè)不可變或可變借用規(guī)則,而通常的規(guī)則檢測(cè)是在編譯階段。
特點(diǎn):
可以在允許出現(xiàn)特定內(nèi)存安全的場(chǎng)景中使用。需要確認(rèn)你的代碼遵守了借用規(guī)則,但是 rust 無法理解只能用于單線程
RefCell<T>
在運(yùn)行時(shí)記錄借用,通過borrow()
和borrow_mut()
方法,會(huì)返回Ref<T>
和RefMut<T>
智能指針,并實(shí)現(xiàn)了Deref
trait.
定義一個(gè)MixName
trait,然后結(jié)構(gòu)體User
實(shí)現(xiàn)了它,并實(shí)現(xiàn)它的方法mix
.
use std::cell::RefCell; pub trait MixName { fn mix(&self, suffix: &str); } struct User { name: RefCell<String>, } impl User { fn new() -> User { User { name: RefCell::new(String::from("hboot")), } } } impl MixName for User { fn mix(&self, suffix: &str) { self.name.borrow_mut().push_str(suffix); } }
mix
方法修改了 self 內(nèi)部屬性name
的值,但是我們可以看到&self
時(shí)不可變引用,這歸功于RefCell<T>
創(chuàng)建值,使得不可變借用可以修改其內(nèi)部值。
fn main(){ let user = User::new(); user.mix(" hello"); println!("{:?}", user.name.borrow()); }
執(zhí)行程序可以看到內(nèi)部的值已經(jīng)被修改了。RefCell<T>
會(huì)在調(diào)用borrow
時(shí),記錄借用次數(shù),當(dāng)離開了作用域時(shí),借用次數(shù)減一。
RefCell<T>
只能有一個(gè)所有者,結(jié)合Rc<T>
使其擁有多個(gè)可變數(shù)據(jù)所有者。
use std::cell::RefCell; use std::rc::Rc; fn main(){ let s = Rc::new(RefCell::new(String::from("hboot"))); let s1 = Rc::clone(&s); let s2 = Rc::clone(&s); *s.borrow_mut() += " good"; println!("{:?}", s); }
通過RefCell
來創(chuàng)建變量,然后通過Rc
開啟多所有權(quán),這樣在*s.borrow_mut() += " good";
,修改后,變量s、s1、s2
的值都發(fā)生了變更。
但是這只能在單線中使用,如果想要多線程使用,則需要使用并發(fā)安全的Mutex<T>
類型。
無畏并發(fā)
并發(fā)編程 - 代表程序的不同部分相互獨(dú)立的運(yùn)行。
并行編程 - 代表程序不同部分同時(shí)執(zhí)行。
thread
多線程運(yùn)行代碼
多線程運(yùn)行代碼可以提高程序的執(zhí)行效率。也會(huì)造成一些問題
- 多個(gè)線程在不同時(shí)刻訪問同一數(shù)據(jù)資源,形成競(jìng)爭(zhēng)
- 相互等待對(duì)方,造成死鎖
- 一些情況下出現(xiàn)的難以修復(fù)的 bug
使用thread::spawn
創(chuàng)建一個(gè)線程,它接受一個(gè)閉包函數(shù)
use std::thread; fn main() { thread::spawn(|| { println!("hello!"); }); println!("rust!"); }
可以看到輸出,先是rust!
,也就是主線程先執(zhí)行??梢远啻螆?zhí)行cargo run
以觀察結(jié)果,會(huì)出現(xiàn)新線程沒有打印輸出,這是因?yàn)橹骶€程結(jié)束,新線程也會(huì)結(jié)束,而不會(huì)等待新線程是否執(zhí)行完畢。
可以通過線程休眠,展示這一特點(diǎn)
use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { thread::sleep(Duration::from_millis(2)); println!("hello!"); }); println!("rust!"); }
程序基本沒有什么機(jī)會(huì)切換到新線程去執(zhí)行,也看不到新線程的打印輸出。
可以通過thread::spawn
的返回值線程實(shí)例,然后調(diào)用join()
方法,來等待線程結(jié)束
let thread = thread::spawn(|| { thread::sleep(Duration::from_millis(2)); println!("hello!"); }); println!("rust!"); thread.join().unwrap();
再次執(zhí)行,可以看到新線程的打印輸出。join()
會(huì)阻塞當(dāng)前線程,知道線程實(shí)例thread
執(zhí)行完畢。
可以將thread.join().unwrap();
放在主線程輸出之前,優(yōu)先執(zhí)行
thread.join().unwrap(); println!("rust!");
通過move
關(guān)鍵字強(qiáng)制閉包獲取其所有權(quán),thread::spawn
創(chuàng)建線程給的閉包函數(shù)沒有任何參數(shù),需要使用主線程里的變量
let name = String::from("hboot"); let thread = thread::spawn(move || { thread::sleep(Duration::from_millis(2)); println!("hello! - {}", name); });
新線程強(qiáng)制獲取了環(huán)境中變量的所有權(quán),保證了新線程執(zhí)行不會(huì)出錯(cuò)。如果是引用,那么由于新線程的執(zhí)行順序,可能會(huì)在主線程執(zhí)行過程使引用失效,從而導(dǎo)致新線程執(zhí)行報(bào)錯(cuò)
線程間消息傳遞
通過channel
實(shí)現(xiàn)線程間消息的傳遞并發(fā)。
通過mpsc::channel
創(chuàng)建通信通道,這個(gè)通道可以有多個(gè)發(fā)送端,但只能有一個(gè)接收端.
use std::sync::mpsc; fn main(){ let (send, receive) = mpsc::channel(); thread::spawn(move || { let name = String::from("rust"); send.send(name).unwrap(); }); let receive_str = receive.recv().unwrap(); println!("get thread msg :{}", receive_str); }
mpsc::channel()
生成一個(gè)通過,返回一個(gè)元組,第一個(gè)是發(fā)送者,第二個(gè)是接收者。然后創(chuàng)建一個(gè)新線程,通過實(shí)例對(duì)象send
發(fā)送一條信息;在主線程中通過實(shí)例對(duì)象receive
接受數(shù)據(jù)。
不管是send()
發(fā)送方法還是recv()
方法,它們都返回Result<T,E>
類型,如果接受端或發(fā)送端被清除了,則會(huì)返回錯(cuò)誤。
接受recv()
方法是阻塞線程的,也就是必須接收到一個(gè)值。還有一個(gè)方法try_recv()
方法則不會(huì)阻塞,需要頻繁去調(diào)用,在有可用消息時(shí)進(jìn)行處理。
新線程將變量name
發(fā)送出去,那么它的所有權(quán)也被轉(zhuǎn)移 出去了,后續(xù)不能使用它
send.send(name).unwrap(); // 在發(fā)送后,不能再使用改變量 println!("{}", name);
當(dāng)在子線程中連續(xù)多次發(fā)送多個(gè)值時(shí),可以通過迭代器遍歷receive
獲取值
fn main(){ let (send, receive) = mpsc::channel(); thread::spawn(move || { send.send(1).unwrap(); send.send(10).unwrap(); send.send(100).unwrap(); }); for receive_str in receive { println!("{}", receive_str); } }
上述例子只是單發(fā)送者,可以通過clone()
方法克隆send
發(fā)送對(duì)象,然后傳給另一個(gè)線程
fn main(){ let (send, receive) = mpsc::channel(); let send_str = send.clone(); thread::spawn(move || { send_str.send("hello").unwrap(); send_str.send("rust").unwrap(); }); thread::spawn(move || { send.send("good").unwrap(); send.send("hboot").unwrap(); }); for receive_str in receive { println!("{}", receive_str); } }
創(chuàng)建兩個(gè)線程,一個(gè)線程傳入時(shí)克隆的send_str
,它們都發(fā)送消息,然后在主線程中,接收到所有消息。
多個(gè)線程由于執(zhí)行順序?qū)е麓蛴≥敵龅捻樞蛞膊槐M相同。這依賴于系統(tǒng),我們可以通過線程休眠做實(shí)驗(yàn),觀察到輸出的順序不同
線程間共享狀態(tài)
除了相互之間發(fā)送消息外, 還可以通過共享數(shù)據(jù),來傳遞數(shù)據(jù)狀態(tài)變化。
通過Mutex<T>
創(chuàng)建共享數(shù)據(jù),在需要使用的線程中通過lock()
獲取鎖,以訪問數(shù)據(jù)。
use std::sync::{Mutex}; fn main()[ let name = Mutex::new(String::from("hboot")); { let mut name = name.lock().unwrap(); *name += " good!"; } println!("{:?}", name.lock().unwrap()); ]
新創(chuàng)建的數(shù)據(jù)hboot
,在局部作用域中獲取鎖,然后解引用后變更值,最終打印輸出可以看到變更后的數(shù)據(jù)。
Mutext<T>
是一個(gè)智能指針,調(diào)用lock()
返回了一個(gè)MutexGuard
智能指針,它實(shí)現(xiàn)了Deref
來指向內(nèi)部數(shù)據(jù),同時(shí)也提供Drop
實(shí)現(xiàn)了當(dāng)離開作用域時(shí)自動(dòng)釋放鎖。
正因?yàn)檫@樣,我們?cè)诰幋a時(shí),不會(huì)因?yàn)橥涐尫沛i而導(dǎo)致其他線程訪問不了數(shù)據(jù)。
如果想要在多個(gè)線程中訪問共享數(shù)據(jù),因?yàn)榫€程需要轉(zhuǎn)移所有權(quán),這樣導(dǎo)致共享數(shù)據(jù)每次只能在一個(gè)線程中使用,通過Arc<T>
來創(chuàng)建多所有者,使得共享數(shù)據(jù)可被多個(gè)線程同時(shí)訪問。
use std::sync::{Arc, Mutex}; use std::thread; fn main(){ let name = Arc::new(Mutex::new(String::from("hboot"))); let mut thread_arr = vec![]; for val in ["admin", "test", "hello", "rust"] { let name = Arc::clone(&name); let thread = thread::spawn(move || { let mut name = name.lock().unwrap(); *name += val; }); thread_arr.push(thread); } for thread in thread_arr { thread.join().unwrap(); } println!("{:?}", name.lock().unwrap()) }
Arc<T>
擁有和Rc<T>
相同的 api,它可以用于并發(fā)環(huán)境的類型。這是一個(gè)原子引用計(jì)數(shù)類型。
Mutex<T>
同RefCell<T>
一樣,提供了內(nèi)部可變性,通過獲取內(nèi)布值的可變引用修改值。當(dāng)然,Mutex<T>
也會(huì)有出現(xiàn)相互引用鎖死的風(fēng)險(xiǎn),兩個(gè)線程需要鎖住兩個(gè)資源而各自已經(jīng)鎖了一個(gè),造成了互相等待的問題。
Sync
和Send trait
擴(kuò)展并發(fā)
除了使用 rust 標(biāo)準(zhǔn)庫(kù)提供的處理并發(fā)問題,還可以使用別人編寫的并發(fā)功能
當(dāng)嘗試編寫并發(fā)功能時(shí),有兩個(gè)并發(fā)概念:
- 通過
Send trait
表明實(shí)現(xiàn)了Send
的類型值的所有權(quán)可以在線程間傳遞。rust 幾乎所有類型都是Send
, 還有一些不能Send
,比如Rc<T>
,它只能用于單線程, - 通過
Sync trait
表明實(shí)現(xiàn)了Sync
的類型可以安全的在多個(gè)線程中擁有其值的引用。Rc<T>、RefCell<T>
都不是Sync
類型的。
根據(jù)這兩個(gè)概念,可以手動(dòng)創(chuàng)建用于并發(fā)功能的并發(fā)類型,在使用時(shí)需要多加小心,以維護(hù)其安全保證。
到此這篇關(guān)于rust 自動(dòng)化測(cè)試、迭代器與閉包、智能指針、無畏并發(fā)的文章就介紹到這了,更多相關(guān)rust 自動(dòng)化測(cè)試內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解讀Rust的Rc<T>:實(shí)現(xiàn)多所有權(quán)的智能指針方式
Rc<T> 是 Rust 中用于多所有權(quán)的引用計(jì)數(shù)類型,通過增加引用計(jì)數(shù)來管理共享數(shù)據(jù),只有當(dāng)最后一個(gè)引用離開作用域時(shí),數(shù)據(jù)才會(huì)被釋放,Rc<T> 適用于單線程環(huán)境,并且只允許不可變共享數(shù)據(jù);需要可變共享時(shí)應(yīng)考慮使用 RefCell<T> 或其他解決方案2025-02-02Rust中FFI編程知識(shí)點(diǎn)整理總結(jié)(推薦)
這篇文章主要介紹了Rust中FFI編程知識(shí)點(diǎn)整理總結(jié),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09關(guān)于使用rust調(diào)用c++靜態(tài)庫(kù)并編譯nodejs包的問題
這篇文章主要介紹了使用rust調(diào)用c++靜態(tài)庫(kù)并編譯nodejs包的問題,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08