網站首頁 編程語言 正文
楔子
Rust 的每個引用都有自己的生命周期,生命周期指的是引用保持有效的作用域。大多數情況下,引用是隱式的、可以被推斷出來的,但當引用可能以不同的方式互相關聯時,則需要手動標注生命周期。
fn?main()?{
????let?r;
????{
????????let?x?=?5;
????????r?=?&x;
????}??//?此處?r?不再有效
????println!("{}",?r);
}
執行的時候會報出如下錯誤:borrowed value does not live long enough,意思就是借用的值存活的時間不夠長。因為把 x 的引用給 r 之后,x 就被銷毀了,那么 r 就成為了一個懸空引用。
而 Rust 會通過借用檢查器,來檢查借用是否合法,顯然上述代碼在執行打印語句的時候,r 已經不合法了。
fn?longest(x:?&str,?y:?&str)?->?&str?{
????if?x.len()?>?y.len()?{
????????x
????}?else?{
????????y?
????}
}
這段代碼也是不合法的,原因就是返回值要么是 x 要么是 y,但具體是哪一個不知道,并且它們的生命周期也都不知道。所以無法通過比較作用域,來判斷返回的引用是否是一致有效的,而借用檢查器也是做不到的,原因就是它不知道返回值的生命周期是跟 x 有關系還是跟 y 有關系。事實上,這個跟函數體的邏輯也沒有關系,函數的聲明就決定了它做不到這一點。
因此我們需要引入生命周期。
生命周期標注語法
首先生命周期標注并不會改變引用的生命長度,當指定了生命周期參數,函數可以接收帶有任何生命周期的引用。生命周期的標注:描述了多個引用的生命周期間的關系,但不影響生命周期本身。
現在光讀起來可能有點繞,別急,一會兒會解釋。
生命周期參數名以?'?開頭,并且名字非常短,通常為 a;標注位置在?& 后面,只有 & 才需要生命周期。因為你引用了一個值,那么這個值的存活時間需要知道,不然人家都被銷毀了還傻傻地用。
- &i32:一個引用;
- &'a i32:帶有顯式生命周期的引用;
- &'a mut i32:帶有顯式生命周期的可變引用;
其實單個生命周期標注本身沒有什么意義,它是為了向 Rust 描述多個具有生命周期的參數之間的關系。并且生命周期和泛型一樣,也要聲明在尖括號內。
//?簽名里面的生命周期必須要有
//?相當于告訴?Rust?有這么一個生命周期?'a
fn?longest<'a>(x:?&'a?str,?y:?&'a?str)?->?&'a?str?{
????if?x.len()?>?y.len()?{
????????x
????}?else?{
????????y
????}
}
此時代碼是合法的,但是注意:我們并沒有改變傳入的值和返回的值的生命周期,我們只是向借用檢查器指出了一些用于檢查非法調用的一些約束而已,而借用檢查器并不需要知道 x、y 的具體存活時長。
而事實上如果函數引用外部的變量,那么單靠 Rust 確定函數和返回值的生命周期幾乎是不可能的事情。因為函數傳遞什么參數都是我們決定的,這樣的話函數在每次調用時使用的生命周期都可能發生變化,正因如此我們才需要手動對生命周期進行標注。
//?準確來說?'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
????}
}
目前是沒有問題的,因為 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);??
此時就報錯了:borrowed value does not live long enough。相信你已經猜到了,因為 x、y 生命周期重疊的部分是 y,返回值 result 的生命周期不能超過它。但當前明顯超過了,所以報錯。
所以說生命周期標注對變量沒有什么影響,它只是給了借用檢查器一個可以用來判斷的約束罷了。
總結一下就是:生命周期用來關聯函數參數和返回值之間的聯系,一旦它們取得了某種聯系,那么 Rust 就獲得了足夠多的信息來保證內存安全的操作,并且阻止那些出現懸空指針或者其它導致內存安全的行為。
到目前為止,你也許還不太了解生命周期,別著急,我們繼續往下看。
結構體中的生命周期標注
struct 里面可以放任意類型,但是不能放引用,比如下面的結構體定義就是錯誤的。
struct?Girl?{
????name:?&str,
????age:?i32
}
結構體如果是合法的,那么它內部的所有成員值都要是合法的。但現在 name 是一個引用,所以結構體實例化的時候一定會引用某個字符串,這就使得字符串存活是結構體實例存活的前提。
但在實際編碼中,這兩者的存活時間沒有什么關系,有可能你在使用結構體實例訪問 name 成員的時候,它引用的字符串都已經被銷毀了。所以 Rust 不允許我們這么做,我們之前是將 name 的類型指定為 String,也就是讓結構體持有全部數據的所有權。?
而如果非要將類型指定為引用的話,那么必須指定生命周期。
//?實例.name?會引用外部的一個字符串,所以要指定生命周期
//?表示字符串的存活時間一定比結構體實例要長
//?否則字符串沒了,而實例還在,那么就會出現懸空引用
#[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);
}
因為指定了生命周期,在編譯的時候借用檢查器就可以檢測出存活時間是否合法。首先 g 的存活時間是整個 main 函數,而 name 的存活時間是內部的花括號那一段作用域,比 g 的存活時間短,因此編譯出錯。
所以通過生命周期標注,Rust 在編譯期間就能通過借用檢查器檢測出引用是否合法,Rust 不會將這種錯誤留到運行時。
生命周期的省略
當一個函數返回了一個引用時,往往需要指定生命周期,而它的目的就是為了保證返回的引用是合法的。如果不合法,在編譯階段就能找出來。
fn?f(s:?&str)?->?&str?{
????s
}
函數參數出現了引用,返回值也有引用,應該指定生命周期呀。是的,在早期版本這段代碼是編譯不過的,它需要你這么寫:
fn?f<'a>(s:?&'a?str)?->?&'a?str?{
????"xxx"
}
但是久而久之,Rust 團隊發現對于這種場景實在沒有必要一遍又一遍的重復編寫生命周期,并且這種只有一個參數完全是可以預測的,有明確的模式。于是 Rust 團隊就將這些模式寫入了借用檢查器,可以自動進行推導,而無需顯式地寫上生命周期標注。
所以在 Rust 引用分析中編入的模式被稱為生命周期省略規則:
- 這些規則無需開發者來遵守;
- 對于一些特殊情況,由編譯器來考慮;
- 如果你的代碼符合這些規則,就無需顯式標注生命周期;
如果生命周期在函數/方法的參數中,則被稱為輸入生命周期;在函數/方法的返回值中,則被稱為輸出生命周期。而 Rust 要能夠在編譯期間基于輸入生命周期,來確定輸出生命周期,如果能夠確定,那么便是合法的。
而當我們省略生命周期時,Rust 就會基于內置的省略規則進行推斷,如果推斷完成后發現引用之間的關系還是模糊不清,就會出現編譯錯誤。而解決辦法就需要我們手動標注生命周期了,表明引用之間的相互關系。
那么 Rust 省略規則到底是怎樣的呢?
- 規則一:每個引用類型的參數都有獨自的生命周期;
- 規則二:如果只有一個參數具有生命周期,或者說只有一個輸入生命周期,那么該生命周期會賦值給所有的輸出生命周期;
- 規則三:如果有多個輸入生命周期,但其中一個是?&self 或 &mut self,那么 self 的生命周期會賦值給所有的輸出生命周期;
如果編譯器在應用完上述三個規則后,能夠計算出返回值的生命周期,則可以省略,否則不能省略。這些規則同樣適用于 fn 定義和 impl 塊,我們來舉幾個例子,感受一下整個過程。
//?函數如下,然后開始應用三個規則
fn?first_word(s:?&str)?->?&str{};
//?1.?每個引用類型的參數都有自己的生命周期,滿足
//????所以函數相當于變成如下
fn?first_word<'a>(s:?&'a?str)?->?&str{};
//?2.?只有一個輸入生命周期,該生命周期被賦給所有的輸出生命周期
//????顯然也是滿足的,所以函數變成如下
fn?first_word<'a>(s:?&'a?str)?->?&'a?str{};
//?3.?不滿足,所以無事發生
應用完三個規則之后,計算出了返回值的生命周期,所以合法。
再舉個例子:
//?函數如下,然后開始應用三個規則
fn?first_word(s1:?&str,?s2:?&str)?->?&str{};
//?1.?每個引用類型的參數都有自己的生命周期
//????顯然滿足,所以函數變成如下
fn?first_word<'a,?'b>(s1:?&'a?str,?s2:?&'b?str)?->?&str{};
//?2.?只有一個輸入生命周期,該生命周期被賦予所有的輸出生命周期
//?但是這里有兩個,所以不滿足
//?3.?不滿足
當編譯器使用了 3 個規則之后仍然無法計算出返回值的生命周期時,就會出現編譯錯誤,顯然上面代碼是會報錯的。我們需要手動標注生命周期:
fn?longest<'a>(x:?&'a?str,?y:?&'a?str)?->?&'a?str?{}
從表面上來看 x、y 的生命周期是相同的,都是 'a,但準確來說它表示的是 x、y 生命周期重疊的部分。而返回值的生命周期標注也是 'a,所以此處的含義就表示輸出生命周期是兩個輸入生命周期重疊的部分。
longest 函數這么改的話,是合法的。
方法中的生命周期標注
然后是在方法中標注生命周期,它的語法和泛型是相似的。
//?聲明周期的語法類似于泛型
//?必須要先通過?<'a>?進行聲明,然后才能使用
struct?Girl?<'a>?{
????name:?&'a?str,
}
//?在學習泛型的時候我們知道
//?這種方式表示為某個類型實現方法
//?現在則變成生命周期,并且?<'a>?不可以省略
impl?<'a>?Girl?<'a>?{
????fn?say_hi(&self)?->?String?{
????????String::from("hello?world")
????}
????//?此處無需指定生命周期,因為?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(""))??//?古明地覺
}
比較簡單,另外程序中還有一個特殊的生命周期叫?'static,它表示整個程序的持續時間。所有的字符串字面量都擁有 'static 生命周期:
fn?main()?{
????let?s:?&'static?str?=?"hello";
}
為引用指定 'static 之前需要三思,是否需要引用在整個程序的生命周期內都存活。
同時指定生命周期和泛型
生命周期的指定方式和泛型是一樣的,那如果想同時指定生命周期和泛型,應該怎么做呢?
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 變量的存活時間,只是給了借用檢查器更多的余地去推斷引用是否合法。
就目前來說,我們介紹的內容都還很基礎,應該很好理解。等把基礎說完了,后面會介紹更多關于 Rust 的細節。最后的最后,我們再一起用 Rust 手寫一個簡易版的 Redis,并和現有的 Redis 做一下性能對比。
原文鏈接:https://mp.weixin.qq.com/s/zcw6ntonEG_2rLJYzxV9CA
相關推薦
- 2022-09-24 Go?select使用與底層原理講解_Golang
- 2022-10-12 Golang中panic的異常處理_Golang
- 2023-04-06 C++中的memset用法詳解_C 語言
- 2022-08-29 Python神器之Pampy模式匹配庫的用法詳解_python
- 2023-12-11 使用SSH地址拉取遠程倉庫代碼報下面的錯誤
- 2022-11-20 golang?實現?pdf?轉高清晰度?jpeg的處理方法_Golang
- 2022-03-19 詳解C語言結構體的定義和使用_C 語言
- 2023-02-10 docker-compose實現容器任務編排的方法步驟_docker
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支