深入了解Rust中引用與借用的用法
楔子
好久沒更新 Rust 了,上一篇文章中我們介紹了 Rust 的所有權(quán),并且最后定義了一個(gè) get_length 函數(shù),但調(diào)用時(shí)會(huì)導(dǎo)致 String 移動(dòng)到函數(shù)體內(nèi)部,而我們又希望在調(diào)用完畢后能繼續(xù)使用該 String,所以不得不使用元組將 String 也作為元素一塊返回。
//?該函數(shù)計(jì)算一個(gè)字符串的長度 fn?get_length(s:?String)?->?(String,?usize)?{ ????//?因?yàn)檫@里的?s?會(huì)獲取變量的所有權(quán) ????//?而一旦獲取,那么調(diào)用方就不能再使用了 ????//?所以我們除了要返回計(jì)算的長度之外 ????//?還要返回這個(gè)字符串本身,也就是將所有權(quán)再交回去 ????let?length?=?s.len(); ????(s,?length) } fn?main()?{ ????let?s?=?String::from("古明地覺"); ????//?接收長度的同時(shí),還要接收字符串本身 ????//?將所有權(quán)重新?"奪"?回來 ????let?(s,?length)?=?get_length(s); ????println!("s?=?{},?length?=?{}",?s,?length);? ????/* ????s?=?古明地覺,?length?=?12 ????*/ }
但這種寫法很笨拙,下面我們將 get_length 函數(shù)重新定義,并學(xué)習(xí) Rust 的引用。
什么是引用
新的函數(shù)簽名使用了 String 的引用作為參數(shù),而沒有直接轉(zhuǎn)移所有權(quán)。
fn?get_length(s:?&String)?->?usize?{ ????s.len() } fn?main()?{ ????let?s1?=?String::from("hello"); ????let?length?=?get_length(&s1); ????println!("s1?=?{},?length?=?{}",?s1,?length);? ????//?s1?=?hello,?length?=?5 }
首先需要注意的是,變量聲明以及函數(shù)返回值中的那些元組代碼都消失了。其次在調(diào)用 get_length 函數(shù)時(shí)使用了 &s1 作為參數(shù),并且在函數(shù)的定義中,我們使用 &String 替代了 String。而 & 代表的就是引用語義,它允許我們?cè)诓猾@取所有權(quán)的前提下使用值。
既然有引用,那么自然就有解引用,它使用 * 作為運(yùn)算符,含義和引用相反,我們會(huì)在后續(xù)詳細(xì)地介紹。
現(xiàn)在,讓我們仔細(xì)觀察一下這個(gè)函數(shù)的調(diào)用過程:
let?s1?=?String::from("hello"); let?length?=?get_length(&s1);
這里的 &s1 允許我們?cè)诓晦D(zhuǎn)移所有權(quán)的前提下,創(chuàng)建一個(gè)指向 s1 值的引用,由于引用不持有值的所有權(quán),所以當(dāng)引用離開當(dāng)前作用域時(shí),它指向的值也不會(huì)被丟棄。同理,函數(shù)簽名中的 & 用來表明參數(shù) s 的類型是一個(gè)引用。
???????????//?s?是一個(gè)指向?String?的引用 fn?get_length(s:?&String)?->?usize?{? ????s.len() }??//?到這里?s?離開作用域 ???//?但由于它并不持有自己指向值的所有權(quán) ???//?所以最終不會(huì)發(fā)生任何事情
此處變量 s 的有效作用域與其它任何函數(shù)參數(shù)一樣,但唯一不同的是,它不會(huì)在離開自己的作用域時(shí)銷毀其指向的數(shù)據(jù),因?yàn)樗⒉粨碛性摂?shù)據(jù)的所有權(quán)。當(dāng)一個(gè)函數(shù)使用引用而不是值本身作為參數(shù)時(shí),我們便不需要為了歸還所有權(quán)而特意去將值返回,畢竟在這種情況下,我們根本沒有取得所有權(quán)。
而將引用傳遞給函數(shù)參數(shù)的這一過程被稱為借用(borrowing),在現(xiàn)實(shí)生活中,假如一個(gè)人擁有某件東西,你可以從他那里把東西借過來。但是當(dāng)你使用完畢時(shí),還必須將東西還回去。
Rust 的變量也是如此,如果一個(gè)值屬于該變量,那么該變量離開作用域時(shí)會(huì)銷毀對(duì)應(yīng)的值,就好比東西你不想要了,你可以將它扔掉,因?yàn)闁|西是你的。但如果是借用的話,變量在離開作用域時(shí),這個(gè)值并不會(huì)被銷毀,就好比東西你不想要了,但這個(gè)東西并不屬于你,因此你要將它還回去,并且這個(gè)東西還在。
至于后續(xù)這個(gè)東西是否會(huì)被扔掉、何時(shí)被扔掉,就看它真正的主人是否還需要它,如果不需要了,東西的主人是有權(quán)利銷毀的,因?yàn)檫@東西是他的。當(dāng)然,他也可以將東西送給別人,此時(shí)就相當(dāng)于發(fā)生了所有權(quán)的轉(zhuǎn)移,轉(zhuǎn)移之后這東西跟他也沒關(guān)系了。
然后問題來了,如果我們嘗試修改借用的值會(huì)怎么樣呢?相信你能猜到,肯定是不允許的,還是拿借東西舉例子,東西既然是借的,就說明你只有使用權(quán),而沒有修改它的權(quán)利。
fn?change_string(s:?&String)?{ ????s.push_str("?world"); } fn?main()?{ ????let?s1?=?String::from("hello"); ????change_string(&s1); }
執(zhí)行這段代碼會(huì)出現(xiàn)編譯錯(cuò)誤:
與變量類似,引用是默認(rèn)不可變的,Rust 不允許我們?nèi)バ薷囊弥赶虻闹怠?/p>
可變引用
我們可以通過一個(gè)小小的調(diào)整來修復(fù)上面的示例中出現(xiàn)的編譯錯(cuò)誤:
fn?change_string(s:?&mut?String)?{ ????s.push_str("?world"); } fn?main()?{ ????let?mut?s1?=?String::from("hello"); ????change_string(&mut?s1); }
首先我們需要將變量 s1 聲明為 mut,即可變的,也就是東西的主人能夠允許它的東西發(fā)生變化。其次,要使用 &mut s1 來給函數(shù)傳入一個(gè)可變引用,意思就是東西的主人在將東西借給別人時(shí)專門強(qiáng)調(diào)了,自己的東西允許修改,不然別人不知道啊。
所以這里如果不傳遞可變引用的話,即使 s1 是可變的,函數(shù) change_string 里面也不能對(duì)值進(jìn)行修改。因此調(diào)用函數(shù)的時(shí)候要傳遞可變引用,當(dāng)然函數(shù)參數(shù)接收的也要是一個(gè)可變引用,因?yàn)轭愋鸵ヅ洹?/p>
另外,除了將引用作為參數(shù)傳遞之外,還可以賦值給一個(gè)變量,因?yàn)樽鳛楹瘮?shù)參數(shù)和賦值給一個(gè)變量是等價(jià)的。
fn?main()?{ ????let?mut?s1?=?String::from("hello"); ????//?可變引用指的是,引用指向的值可以修改 ????//?所以要注意這里的寫法,不要寫成了?let?mut?s2:?&String ????//?這表示?s2?是個(gè)不可變引用,但?s2?本身是可變的 ????//?可變引用是一個(gè)整體,所以?&mut?String?要整體作為?s2?的類型 ????let?s2:?&mut?String?=?&mut?s1; ????//?當(dāng)然啦,此時(shí)?s2?引用的值可變,但?s2?本身不可變 ????//?如果希望?s2?還能接收其它字符串的可變引用,那么應(yīng)該這么聲明 ????//?let?mut?s2:?&mut?String?=?&mut?s1; ????//?此時(shí)表示?s2?是個(gè)可變引用,它引用的值可以修改 ????//?并且 s2 本身也是可變的?;蛘哌€有更簡單的寫法: ????//?直接寫成?let?mut s2?=?&mut?s1?也行,因?yàn)?Rust?會(huì)做類型推斷 ??? ????s2.push_str("?world"); ????println!("{}",?s1);??//?hello?world }
此外要注意:當(dāng)變量聲明為不可變時(shí),只能創(chuàng)建不可變引用。
fn?main()?{ ????let?s1?=?String::from("hello"); ????let?s2:?&mut?String?=?&mut?s1; ????println!("{}",?s2);? }
代碼中的 s1 不可變,但卻創(chuàng)建了可變引用,于是報(bào)錯(cuò)。
因?yàn)?s1 是不可變的,就意味著數(shù)據(jù)(包括棧內(nèi)存、堆內(nèi)存)不可以修改,所以此時(shí)不能創(chuàng)建可變引用,否則就意味著值是可以修改的,于是就矛盾了。因此當(dāng)變量聲明為不可變時(shí),不可以將可變引用賦值給其它變量。
但當(dāng)變量聲明為可變時(shí),既可以創(chuàng)建可變引用,也可以創(chuàng)建不可變引用。如果是可變引用,那么允許通過引用修改值;如果是不可變引用,那么不允許通過引用修改值。
fn?main()?{ ????//?變量可變 ????let?mut?s1?=?String::from("hello"); ????//?可以通過?&s1?創(chuàng)建不可變引用 ????//?也可以通過?&mut?s1?創(chuàng)建可變引用 ????//?但前者不可以修改值,后者可以 }
另外可變引用有一個(gè)很大的限制:對(duì)于特定作用域中的特定數(shù)據(jù)來說,一次只能聲明一個(gè)可變引用,否則會(huì)導(dǎo)致編譯錯(cuò)誤。
fn?main()?{ ????let?mut?s1?=?String::from("hello"); ????let?s2?=?&mut?s1; ????let?s3?=?&mut?s1; ????s2.push_str("xx"); ????s3.push_str("yy"); ????println!("{}",?s1); }
我們將 s1 的可變引用給了 s2 之后又給了 s3,而這是非法的。
但 Rust 做了一個(gè) "容忍" 操作,那就是聲明多個(gè)引用之后,如果都不使用的話,那么也不會(huì)出現(xiàn)錯(cuò)誤。
fn?main()?{ ????let?mut?s1?=?String::from("hello"); ????let?s2?=?&mut?s1; ????let?s3?=?&mut?s1; ????println!("{}",?s1);??//?hello }
以上這段代碼可以順利執(zhí)行,雖然聲明了多個(gè)可變引用,但我們沒有使用,所以 Rust 編譯器就大發(fā)慈悲 "饒" 了我們。但只要對(duì)任意某個(gè)引用執(zhí)行了任意某個(gè)操作,那么 Rust 就不會(huì)再手下留情了,比如:
fn?main()?{ ????let?mut?s1?=?String::from("hello"); ????let?s2?=?&mut?s1; ????let?s3?=?&mut?s1; ????println!("{}",?s2);? }
我們上面對(duì) s2 執(zhí)行了打印操作,于是 Rust 就會(huì)提示我們可變引用只能被借用一次。
但說實(shí)話 Rust 編譯器做的這個(gè) "忍讓" 對(duì)于我們而言沒有太大意義,因?yàn)樗笪覀兟暶鞫鄠€(gè)可變引用之后不能使用其中的任何一個(gè),但問題是聲明引用就是為了使用它,不然聲明它干嘛。因此我們?nèi)钥梢哉J(rèn)為:對(duì)于特定作用域中的特定數(shù)據(jù)來說,一次只能聲明一個(gè)可變引用,否則會(huì)導(dǎo)致編譯錯(cuò)誤。
這個(gè)規(guī)則使得引用的可變性只能以一種受到嚴(yán)格限制的方式來使用,許多剛剛接觸 Rust 的開發(fā)者會(huì)反復(fù)地與它進(jìn)行斗爭,因?yàn)榇蟛糠值恼Z言都允許你隨意修改變量。但另一方面,在 Rust 中遵循這條限制性規(guī)則可以幫助我們?cè)诰幾g時(shí)避免數(shù)據(jù)競爭。數(shù)據(jù)競爭(data race)與競態(tài)條件十分類似,它會(huì)在指令同時(shí)滿足以下 3 種情形時(shí)發(fā)生:
- 兩個(gè)或兩個(gè)以上的指針同時(shí)訪問同一空間;
- 其中至少有一個(gè)指針會(huì)向空間中寫入數(shù)據(jù);
- 沒有同步數(shù)據(jù)訪問的機(jī)制;
數(shù)據(jù)競爭會(huì)導(dǎo)致未定義的行為,由于這些未定義的行為往往難以在運(yùn)行時(shí)進(jìn)行跟蹤,也就使得出現(xiàn)的 bug 更加難以被診斷和修復(fù)。Rust 則完美地避免了這種情形的出現(xiàn),因?yàn)榇嬖跀?shù)據(jù)競爭的代碼連編譯檢查都無法通過??。
與大部分語言類似,我們可以通過花括號(hào)來創(chuàng)建一個(gè)新的作用域范圍,這就使我們可以創(chuàng)建多個(gè)可變引用,當(dāng)然,同一時(shí)刻只允許有一個(gè)可變引用。
fn?main()?{ ????let?mut?s1?=?String::from("hello"); ????{ ????????let?s2?=?&mut?s1; ????????s2.push_str("?cruel"); ????????println!("s2?=?{}",?s2); ????????println!("s1?=?{}",?s1); ????} ????//?這個(gè)?s3?不能聲明在上面的大括號(hào)之前,也就是不能先聲明?s3 ????//?因?yàn)橄嚷暶?s3?的話,那么聲明?s2?的時(shí)候就會(huì)出現(xiàn)兩個(gè)可變引用 ????//?違反了同一時(shí)刻只能有一個(gè)可變引用的原則 ????//?但是將?s3?聲明在這里就沒有問題,因?yàn)槁暶?s2?的時(shí)候?s3?還不存在 ????//?聲明?s3?的時(shí)候?s2?已經(jīng)失效了 ????//?所以此時(shí)滿足同一時(shí)刻只能有一個(gè)可變引用的原則,我生君未生、君生我已死 ????let?s3?=?&mut?s1; ????s3.push_str("?world"); ????println!("s3?=?{}",?s3);?? ????println!("s1?=?{}",?s1);?? ????/* ????s2?=?hello?cruel ????s1?=?hello?cruel ????s3?=?hello?cruel?world ????s1?=?hello?cruel?world ?????*/ }
注意:我們一直說的"一個(gè)可變引用"、"多個(gè)可變引用",它們針對(duì)的都是同一變量;如果是多個(gè)彼此無關(guān)的變量,那么它們的可變引用之間也沒有關(guān)系,此時(shí)是可以共存的。比如同一時(shí)刻有 N 個(gè)可變引用,但它們引用的都是不同的變量,所以此時(shí)沒有問題。
我們一直說的不允許存在多個(gè)可變引用,指的是同一變量的多個(gè)可變引用,這一點(diǎn)要分清楚。
如果是編程老手的話,那么應(yīng)該會(huì)想到,如果同時(shí)存在可變引用和不可變引用會(huì)發(fā)生什么呢?我們?cè)囈幌戮椭懒恕?/p>
fn?main()?{ ????let?mut?s1?=?String::from("hello"); ????let?s2?=?&s1; ????let?s3?=?&mut?s1; ????println!("{}",?s2); ????println!("{}",?s3) }
所以在結(jié)合使用可變引用與不可變引用時(shí),還有一條類似的限制規(guī)則,我們不能在擁有不可變引用的同時(shí)創(chuàng)建可變引用,否則不可變引用就沒有意義了。但同時(shí)存在多個(gè)不可變引用是合理合法的,數(shù)據(jù)的讀操作之間不會(huì)彼此影響。
就有點(diǎn)類似于讀鎖和寫鎖的關(guān)系。
盡管這些編譯錯(cuò)誤會(huì)讓人不時(shí)地感到沮喪,但是請(qǐng)牢記一點(diǎn):Rust 編譯器可以為我們提早(在編譯時(shí)而不是運(yùn)行時(shí))暴露那些潛在的bug,并且明確指出出現(xiàn)問題的地方。你不再需要去追蹤調(diào)試為何數(shù)據(jù)會(huì)在運(yùn)行時(shí)發(fā)生了非預(yù)期的變化。
懸空引用
使用擁有指針概念的語言會(huì)非常容易錯(cuò)誤地創(chuàng)建出懸空指針,這類指針指向曾經(jīng)存在的某處內(nèi)存,但現(xiàn)在該內(nèi)存已經(jīng)被釋放掉、或者被重新分配另作他用了。而在 Rust 語言中,編譯器會(huì)確保引用永遠(yuǎn)不會(huì)進(jìn)入這種懸空狀態(tài),假如我們當(dāng)前持有某個(gè)數(shù)據(jù)的引用,那么編譯器可以保證這個(gè)數(shù)據(jù)不會(huì)在引用被銷毀前離開自己的作用域。
讓我們?cè)囍鴣韯?chuàng)建一個(gè)懸空引用,并看一看 Rust 是如何在編譯期發(fā)現(xiàn)這個(gè)錯(cuò)誤的:
fn?dangle()?->?&String?{ ????let?s?=?String::from("hello?world"); ????&s } fn?main()?{ ???? }
出現(xiàn)的錯(cuò)誤如下所示:
這段錯(cuò)誤的提示信息包含了一個(gè)我們還沒有接觸的概念:生命周期,我們會(huì)后續(xù)詳細(xì)討論它。但即使不考慮生命周期,甚至不看錯(cuò)誤提示,我們也知道原因。dangle 里面的字符串 s 在函數(shù)結(jié)束后就會(huì)失效,內(nèi)存會(huì)回收,但我們卻返回了它的引用。
此處和 C 就出現(xiàn)了不同,C 中的堆內(nèi)存如果我們不手動(dòng)釋放,那么它是不會(huì)自己釋放的。而 Rust 中的堆內(nèi)存會(huì)在變量離開作用域的時(shí)候自動(dòng)回收,既然回收了,那么再返回它的引用就不對(duì)了,因?yàn)橹赶虻膬?nèi)存是無效的。所以我們也能猜到生命周期是做什么的,后續(xù)聊。
而這個(gè)問題的解決辦法也很簡單,直接返回 String 就好。
fn?dangle()?->?String?{ ????let?s?=?String::from("hello?world"); ????s }
這種寫法沒有任何問題,因?yàn)樗袡?quán)從 dangle 函數(shù)中被轉(zhuǎn)移出去了,自然也就不會(huì)涉及釋放操作了。
小結(jié)
讓我們簡要地概括一下對(duì)引用的討論:
在任何一段給定的時(shí)間里,要么只能擁有一個(gè)可變引用,要么只能擁有任意數(shù)量的不可變引用;
引用總是有效的;
到此這篇關(guān)于深入了解Rust中引用與借用的用法的文章就介紹到這了,更多相關(guān)Rust引用 借用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
rust實(shí)現(xiàn)post小程序(完整代碼)
這篇文章主要介紹了rust實(shí)現(xiàn)一個(gè)post小程序,本文通過示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-04-04Rust動(dòng)態(tài)數(shù)組Vec基本概念及用法
Rust中的Vec是一種動(dòng)態(tài)數(shù)組,它可以在運(yùn)行時(shí)自動(dòng)調(diào)整大小,本文主要介紹了Rust動(dòng)態(tài)數(shù)組Vec基本概念及用法,具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12Rust中的Cargo構(gòu)建、運(yùn)行、調(diào)試
Cargo是rustup安裝后自帶的,Cargo?是?Rust?的構(gòu)建系統(tǒng)和包管理器,這篇文章主要介紹了Rust之Cargo構(gòu)建、運(yùn)行、調(diào)試,需要的朋友可以參考下2022-09-09