深入了解Rust的生命周期
楔子
Rust 的每個(gè)引用都有自己的生命周期,生命周期指的是引用保持有效的作用域。大多數(shù)情況下,引用是隱式的、可以被推斷出來的,但當(dāng)引用可能以不同的方式互相關(guān)聯(lián)時(shí),則需要手動標(biāo)注生命周期。
fn?main()?{ ????let?r; ????{ ????????let?x?=?5; ????????r?=?&x; ????}??//?此處?r?不再有效 ????println!("{}",?r); }
執(zhí)行的時(shí)候會報(bào)出如下錯(cuò)誤:borrowed value does not live long enough,意思就是借用的值存活的時(shí)間不夠長。因?yàn)榘?x 的引用給 r 之后,x 就被銷毀了,那么 r 就成為了一個(gè)懸空引用。
而 Rust 會通過借用檢查器,來檢查借用是否合法,顯然上述代碼在執(zhí)行打印語句的時(shí)候,r 已經(jīng)不合法了。
fn?longest(x:?&str,?y:?&str)?->?&str?{ ????if?x.len()?>?y.len()?{ ????????x ????}?else?{ ????????y? ????} }
這段代碼也是不合法的,原因就是返回值要么是 x 要么是 y,但具體是哪一個(gè)不知道,并且它們的生命周期也都不知道。所以無法通過比較作用域,來判斷返回的引用是否是一致有效的,而借用檢查器也是做不到的,原因就是它不知道返回值的生命周期是跟 x 有關(guān)系還是跟 y 有關(guān)系。事實(shí)上,這個(gè)跟函數(shù)體的邏輯也沒有關(guān)系,函數(shù)的聲明就決定了它做不到這一點(diǎn)。
因此我們需要引入生命周期。
生命周期標(biāo)注語法
首先生命周期標(biāo)注并不會改變引用的生命長度,當(dāng)指定了生命周期參數(shù),函數(shù)可以接收帶有任何生命周期的引用。生命周期的標(biāo)注:描述了多個(gè)引用的生命周期間的關(guān)系,但不影響生命周期本身。
現(xiàn)在光讀起來可能有點(diǎn)繞,別急,一會兒會解釋。
生命周期參數(shù)名以 ' 開頭,并且名字非常短,通常為 a;標(biāo)注位置在 & 后面,只有 & 才需要生命周期。因?yàn)槟阋昧艘粋€(gè)值,那么這個(gè)值的存活時(shí)間需要知道,不然人家都被銷毀了還傻傻地用。
- &i32:一個(gè)引用;
- &'a i32:帶有顯式生命周期的引用;
- &'a mut i32:帶有顯式生命周期的可變引用;
其實(shí)單個(gè)生命周期標(biāo)注本身沒有什么意義,它是為了向 Rust 描述多個(gè)具有生命周期的參數(shù)之間的關(guān)系。并且生命周期和泛型一樣,也要聲明在尖括號內(nèi)。
//?簽名里面的生命周期必須要有 //?相當(dāng)于告訴?Rust?有這么一個(gè)生命周期?'a fn?longest<'a>(x:?&'a?str,?y:?&'a?str)?->?&'a?str?{ ????if?x.len()?>?y.len()?{ ????????x ????}?else?{ ????????y ????} }
此時(shí)代碼是合法的,但是注意:我們并沒有改變傳入的值和返回的值的生命周期,我們只是向借用檢查器指出了一些用于檢查非法調(diào)用的一些約束而已,而借用檢查器并不需要知道 x、y 的具體存活時(shí)長。
而事實(shí)上如果函數(shù)引用外部的變量,那么單靠 Rust 確定函數(shù)和返回值的生命周期幾乎是不可能的事情。因?yàn)楹瘮?shù)傳遞什么參數(shù)都是我們決定的,這樣的話函數(shù)在每次調(diào)用時(shí)使用的生命周期都可能發(fā)生變化,正因如此我們才需要手動對生命周期進(jìn)行標(biāo)注。
//?準(zhǔn)確來說?'a?指的就是?x?和?y?生命周期重疊的那一部分 //?而返回值的生命周期不能超重疊的部分 fn?longest<'a>(x:?&'a?str,?y:?&'a?str)?->?&'a?str?{ ????if?x.len()?>?y.len()?{ ????????x ????}?else?{ ????????y ????} } fn?main()?{ ????let?x?=?String::from("hello"); ????{ ????????let?y?=?String::from("satori"); ????????let?result?=?longest(&x,?&y); ????????println!("result?=?{}",?result); ????????//?result?=?satori ????} }
目前是沒有問題的,因?yàn)?x 和 y 的生命周期重疊的部分是 y,然后返回值 result 和 y 也是一樣的。但如果我們把代碼改一下,將 println! 語句移到花括號外面:
fn?longest<'a>(x:?&'a?str,?y:?&'a?str)?->?&'a?str?{ ????if?x.len()?>?y.len()?{ ????????x ????}?else?{ ????????y ????} } fn?main()?{ ????let?x?=?"hello".to_string(); ????let?result; ????{ ????????let?y?=?"satori".to_string(); ????????result?=?longest(&x,?&y); ????} ????println!("result?=?{}",?result);??
此時(shí)就報(bào)錯(cuò)了:borrowed value does not live long enough。相信你已經(jīng)猜到了,因?yàn)?x、y 生命周期重疊的部分是 y,返回值 result 的生命周期不能超過它。但當(dāng)前明顯超過了,所以報(bào)錯(cuò)。
所以說生命周期標(biāo)注對變量沒有什么影響,它只是給了借用檢查器一個(gè)可以用來判斷的約束罷了。
總結(jié)一下就是:生命周期用來關(guān)聯(lián)函數(shù)參數(shù)和返回值之間的聯(lián)系,一旦它們?nèi)〉昧四撤N聯(lián)系,那么 Rust 就獲得了足夠多的信息來保證內(nèi)存安全的操作,并且阻止那些出現(xiàn)懸空指針或者其它導(dǎo)致內(nèi)存安全的行為。
到目前為止,你也許還不太了解生命周期,別著急,我們繼續(xù)往下看。
結(jié)構(gòu)體中的生命周期標(biāo)注
struct 里面可以放任意類型,但是不能放引用,比如下面的結(jié)構(gòu)體定義就是錯(cuò)誤的。
struct?Girl?{ ????name:?&str, ????age:?i32 }
結(jié)構(gòu)體如果是合法的,那么它內(nèi)部的所有成員值都要是合法的。但現(xiàn)在 name 是一個(gè)引用,所以結(jié)構(gòu)體實(shí)例化的時(shí)候一定會引用某個(gè)字符串,這就使得字符串存活是結(jié)構(gòu)體實(shí)例存活的前提。
但在實(shí)際編碼中,這兩者的存活時(shí)間沒有什么關(guān)系,有可能你在使用結(jié)構(gòu)體實(shí)例訪問 name 成員的時(shí)候,它引用的字符串都已經(jīng)被銷毀了。所以 Rust 不允許我們這么做,我們之前是將 name 的類型指定為 String,也就是讓結(jié)構(gòu)體持有全部數(shù)據(jù)的所有權(quán)。
而如果非要將類型指定為引用的話,那么必須指定生命周期。
//?實(shí)例.name?會引用外部的一個(gè)字符串,所以要指定生命周期 //?表示字符串的存活時(shí)間一定比結(jié)構(gòu)體實(shí)例要長 //?否則字符串沒了,而實(shí)例還在,那么就會出現(xiàn)懸空引用 #[derive(Debug)] struct?Girl<'a>?{ ????name:?&'a?str, ????age:?i32 } fn?main()?{ ????let?g; ????{ ????????let?name?=?String::from("古明地覺"); ????????g?=?Girl{name:?&name,?age:?16}; ????} ????println!("{:?}",?g); }
因?yàn)橹付松芷冢诰幾g的時(shí)候借用檢查器就可以檢測出存活時(shí)間是否合法。首先 g 的存活時(shí)間是整個(gè) main 函數(shù),而 name 的存活時(shí)間是內(nèi)部的花括號那一段作用域,比 g 的存活時(shí)間短,因此編譯出錯(cuò)。
所以通過生命周期標(biāo)注,Rust 在編譯期間就能通過借用檢查器檢測出引用是否合法,Rust 不會將這種錯(cuò)誤留到運(yùn)行時(shí)。
生命周期的省略
當(dāng)一個(gè)函數(shù)返回了一個(gè)引用時(shí),往往需要指定生命周期,而它的目的就是為了保證返回的引用是合法的。如果不合法,在編譯階段就能找出來。
fn?f(s:?&str)?->?&str?{ ????s }
函數(shù)參數(shù)出現(xiàn)了引用,返回值也有引用,應(yīng)該指定生命周期呀。是的,在早期版本這段代碼是編譯不過的,它需要你這么寫:
fn?f<'a>(s:?&'a?str)?->?&'a?str?{ ????"xxx" }
但是久而久之,Rust 團(tuán)隊(duì)發(fā)現(xiàn)對于這種場景實(shí)在沒有必要一遍又一遍的重復(fù)編寫生命周期,并且這種只有一個(gè)參數(shù)完全是可以預(yù)測的,有明確的模式。于是 Rust 團(tuán)隊(duì)就將這些模式寫入了借用檢查器,可以自動進(jìn)行推導(dǎo),而無需顯式地寫上生命周期標(biāo)注。
所以在 Rust 引用分析中編入的模式被稱為生命周期省略規(guī)則:
- 這些規(guī)則無需開發(fā)者來遵守;
- 對于一些特殊情況,由編譯器來考慮;
- 如果你的代碼符合這些規(guī)則,就無需顯式標(biāo)注生命周期;
如果生命周期在函數(shù)/方法的參數(shù)中,則被稱為輸入生命周期;在函數(shù)/方法的返回值中,則被稱為輸出生命周期。而 Rust 要能夠在編譯期間基于輸入生命周期,來確定輸出生命周期,如果能夠確定,那么便是合法的。
而當(dāng)我們省略生命周期時(shí),Rust 就會基于內(nèi)置的省略規(guī)則進(jìn)行推斷,如果推斷完成后發(fā)現(xiàn)引用之間的關(guān)系還是模糊不清,就會出現(xiàn)編譯錯(cuò)誤。而解決辦法就需要我們手動標(biāo)注生命周期了,表明引用之間的相互關(guān)系。
那么 Rust 省略規(guī)則到底是怎樣的呢?
- 規(guī)則一:每個(gè)引用類型的參數(shù)都有獨(dú)自的生命周期;
- 規(guī)則二:如果只有一個(gè)參數(shù)具有生命周期,或者說只有一個(gè)輸入生命周期,那么該生命周期會賦值給所有的輸出生命周期;
- 規(guī)則三:如果有多個(gè)輸入生命周期,但其中一個(gè)是 &self 或 &mut self,那么 self 的生命周期會賦值給所有的輸出生命周期;
如果編譯器在應(yīng)用完上述三個(gè)規(guī)則后,能夠計(jì)算出返回值的生命周期,則可以省略,否則不能省略。這些規(guī)則同樣適用于 fn 定義和 impl 塊,我們來舉幾個(gè)例子,感受一下整個(gè)過程。
//?函數(shù)如下,然后開始應(yīng)用三個(gè)規(guī)則 fn?first_word(s:?&str)?->?&str{}; //?1.?每個(gè)引用類型的參數(shù)都有自己的生命周期,滿足 //????所以函數(shù)相當(dāng)于變成如下 fn?first_word<'a>(s:?&'a?str)?->?&str{}; //?2.?只有一個(gè)輸入生命周期,該生命周期被賦給所有的輸出生命周期 //????顯然也是滿足的,所以函數(shù)變成如下 fn?first_word<'a>(s:?&'a?str)?->?&'a?str{}; //?3.?不滿足,所以無事發(fā)生
應(yīng)用完三個(gè)規(guī)則之后,計(jì)算出了返回值的生命周期,所以合法。
再舉個(gè)例子:
//?函數(shù)如下,然后開始應(yīng)用三個(gè)規(guī)則 fn?first_word(s1:?&str,?s2:?&str)?->?&str{}; //?1.?每個(gè)引用類型的參數(shù)都有自己的生命周期 //????顯然滿足,所以函數(shù)變成如下 fn?first_word<'a,?'b>(s1:?&'a?str,?s2:?&'b?str)?->?&str{}; //?2.?只有一個(gè)輸入生命周期,該生命周期被賦予所有的輸出生命周期 //?但是這里有兩個(gè),所以不滿足 //?3.?不滿足
當(dāng)編譯器使用了 3 個(gè)規(guī)則之后仍然無法計(jì)算出返回值的生命周期時(shí),就會出現(xiàn)編譯錯(cuò)誤,顯然上面代碼是會報(bào)錯(cuò)的。我們需要手動標(biāo)注生命周期:
fn?longest<'a>(x:?&'a?str,?y:?&'a?str)?->?&'a?str?{}
從表面上來看 x、y 的生命周期是相同的,都是 'a,但準(zhǔn)確來說它表示的是 x、y 生命周期重疊的部分。而返回值的生命周期標(biāo)注也是 'a,所以此處的含義就表示輸出生命周期是兩個(gè)輸入生命周期重疊的部分。
longest 函數(shù)這么改的話,是合法的。
方法中的生命周期標(biāo)注
然后是在方法中標(biāo)注生命周期,它的語法和泛型是相似的。
//?聲明周期的語法類似于泛型 //?必須要先通過?<'a>?進(jìn)行聲明,然后才能使用 struct?Girl?<'a>?{ ????name:?&'a?str, } //?在學(xué)習(xí)泛型的時(shí)候我們知道 //?這種方式表示為某個(gè)類型實(shí)現(xiàn)方法 //?現(xiàn)在則變成生命周期,并且?<'a>?不可以省略 impl?<'a>?Girl?<'a>?{ ????fn?say_hi(&self)?->?String?{ ????????String::from("hello?world") ????} ????//?此處無需指定生命周期,因?yàn)?Rust?可以推斷出來 ????//?會自動將?self?的生命周期賦值給所有的輸出生命周期 ????fn?get_name(&self,?useless_arg:?&str)?->?&str?{ ????????self.name ????} } fn?main()?{ ????let?name?=?String::from("古明地覺"); ????let?g?=?Girl{name:&name}; ????println!("{}",?g.say_hi());??//?hello?world ????println!("{}",?g.get_name(""))??//?古明地覺 }
比較簡單,另外程序中還有一個(gè)特殊的生命周期叫 'static,它表示整個(gè)程序的持續(xù)時(shí)間。所有的字符串字面量都擁有 'static 生命周期:
fn?main()?{ ????let?s:?&'static?str?=?"hello"; }
為引用指定 'static 之前需要三思,是否需要引用在整個(gè)程序的生命周期內(nèi)都存活。
同時(shí)指定生命周期和泛型
生命周期的指定方式和泛型是一樣的,那如果想同時(shí)指定生命周期和泛型,應(yīng)該怎么做呢?
fn?largest<'a,?T>(x:?&'a?str,?y:?&'a?str, ??????????????????useless_arg:?T)?->?&'a?str?{ ????if?x?>?y?{ ????????x ????}?else?{ ????????y ????} } fn?main()?{ ????let?s1?=?"hello"; ????let?s2?=?"hellO"; ????println!("{}",?largest(s1,?s2,?"")); ????//?hello }
非常簡單,但要保證生命周期在前,泛型在后。
以上就是 Rust 的生命周期,它并沒有改變 Rust 變量的存活時(shí)間,只是給了借用檢查器更多的余地去推斷引用是否合法。
就目前來說,我們介紹的內(nèi)容都還很基礎(chǔ),應(yīng)該很好理解。等把基礎(chǔ)說完了,后面會介紹更多關(guān)于 Rust 的細(xì)節(jié)。最后的最后,我們再一起用 Rust 手寫一個(gè)簡易版的 Redis,并和現(xiàn)有的 Redis 做一下性能對比。
到此這篇關(guān)于深入了解Rust的生命周期的文章就介紹到這了,更多相關(guān)Rust 生命周期內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于使用rust調(diào)用c++靜態(tài)庫并編譯nodejs包的問題
這篇文章主要介紹了使用rust調(diào)用c++靜態(tài)庫并編譯nodejs包的問題,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08Rust調(diào)用函數(shù)操作符?.?和?::?的區(qū)別詳解
在Rust中,.和::操作符都可以用來調(diào)用方法,但它們的用法有所不同,所以本文就將詳細(xì)的給大家介紹一下.和::操作符的區(qū)別,感興趣的同學(xué)跟著小編一起來學(xué)習(xí)吧2023-07-07Rust調(diào)用C程序的實(shí)現(xiàn)步驟
本文主要介紹了Rust調(diào)用C程序的實(shí)現(xiàn)步驟,包括創(chuàng)建C函數(shù)、編譯C代碼、鏈接Rust和C代碼等步驟,具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12Rust 數(shù)據(jù)分析利器polars用法詳解
這篇文章主要介紹了Rust 數(shù)據(jù)分析利器polars用法詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-08-08為什么要使用 Rust 語言、Rust 語言有什么優(yōu)勢
雖然 Rust 是一種通用的多范式語言,但它的目標(biāo)是 C 和 C++占主導(dǎo)地位的系統(tǒng)編程領(lǐng)域,很多朋友會問rust語言難學(xué)嗎?rust語言可以做什么,今天帶著這些疑問通過本文詳細(xì)介紹下,感興趣的朋友一起看看吧2022-10-10