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