如何在十分鐘內(nèi)掌握Rust引用?
近年來,Rust已經(jīng)迅速成為最流行和增長(zhǎng)最快的編程語言之一。谷歌和微軟等大型科技公司正在使用和投資它。它是一種允許帶有特殊約束的手動(dòng)內(nèi)存管理的語言,這在很大程度上確保了內(nèi)存安全。
然而,Rust使用的約束(通常稱為借用檢查器)可能非常難以學(xué)習(xí)。嗯,如果你沒有正確學(xué)習(xí)的話。
這篇文章可以使你快速學(xué)習(xí)Rust中正確的引用概念。前提是你有一些Rust的基礎(chǔ)知識(shí),比如結(jié)構(gòu)體、函數(shù)和向量。
什么是引用?
引用是指在不顯式復(fù)制的情況下引用某些數(shù)據(jù)或變量的方法。Rust的引用與C和C++中的非混淆指針相同。在C和C++中,非混淆指針都是用restrict關(guān)鍵字定義的。在Rust中,引用采用的正是這種行為。但是,任何使引用相互命名別名的嘗試,無論是使用unsafe塊還是使用Rust的指針(這是另一個(gè)主題),都將導(dǎo)致未定義的行為。不要這樣做。
在Rust中,有四種方法可以將變量“傳遞”或轉(zhuǎn)移到函數(shù)或作用域之外。
1,移動(dòng)變量:默認(rèn)情況下,Rust會(huì)在賦值或從函數(shù)返回值時(shí)移動(dòng)值。移動(dòng)意味著一旦變量被移動(dòng),就不能在之前的位置使用它。
2,傳遞不可變引用:不可變引用是一種從另一個(gè)作用域引用變量的方法,只要該引用不會(huì)超出它所引用的變量的作用域。在Rust中,這被稱為生命周期??梢杂幸粋€(gè)或多個(gè)對(duì)變量的不可變引用。
3,傳遞可變引用:可變引用是引用來自另一個(gè)作用域的變量的一種方式,適用于類似的生命周期規(guī)則。但是,一個(gè)變量一次只有一個(gè)可變引用。這意味著在任何給定時(shí)間,任何變量都只能通過單個(gè)引用進(jìn)行修改。
4,傳遞副本:在Rust中,不同的類型可以實(shí)現(xiàn)Copy或Clone特征,這樣它們就可以隱式或顯式地復(fù)制。Copy和Clone之間的主要區(qū)別在于前者是一個(gè)字節(jié)一個(gè)字節(jié)的memcpy風(fēng)格復(fù)制,而Clone是顯式實(shí)現(xiàn)的一個(gè)成員一個(gè)成員的復(fù)制,可以使用自定義邏輯。
規(guī)則
引用的第一個(gè)也是最重要的規(guī)則是只有一個(gè)可變引用或多個(gè)不可變引用。但有一個(gè)問題是,這在實(shí)踐中看起來如何?讓我們來看幾個(gè)例子,從下面這個(gè)開始:
fn main() {
let mut a = 6;
let b = &a;
let c = &mut a;
println!("{}", *c);
}
上面的代碼實(shí)際上是有效的,你可能會(huì)認(rèn)為同時(shí)存在不可變引用和可變引用。然而,需要注意的是,代碼只使用了c,沒有使用b下的不可變引用。由于這個(gè)原因,Rust的借用檢查器不會(huì)報(bào)錯(cuò)。但是讓我們看看當(dāng)我們開始使用b時(shí)會(huì)發(fā)生什么:
fn main() {
let mut a = 6;
let b = &a;
let c = &mut a;
println!("{}", *b);
}
這會(huì)導(dǎo)致編譯失?。?/p>
error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable
--> src/main.rs:7:13
|
6 | let b = &a;
| -- immutable borrow occurs here
7 | let c = &mut a;
| ^^^^^^ mutable borrow occurs here
8 | println!("{}", *b);
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
b被println!借走了,這會(huì)導(dǎo)致不可變和可變引用不能同時(shí)存在的規(guī)則被打破。
接下來,讓我們看一個(gè)更復(fù)雜的例子:
fn main() {
let mut a = 6;
let mut b = &a;
let c = &mut b;
println!("{}", *c);
}
乍一看,這看起來像是對(duì)同一個(gè)變量取了一個(gè)可變引用和一個(gè)不可變引用。然而,理解引用既是類型又是操作符是至關(guān)重要的。當(dāng)使用引用操作符時(shí),它接受與該操作符一起使用的變量的引用。
這意味著,c是對(duì)整數(shù)引用的可變引用。這個(gè)引用的Rust類型看起來像&mut&usize。在上面的代碼中,c可以被解引用并指向一個(gè)不同的&usize引用,這個(gè)引用會(huì)改變b,但不會(huì)改變a。如果我們?cè)噲D通過c來改變a,如下:
fn main() {
let mut a = 6;
let mut b = &a;
let c = &mut b;
**c += 1;
println!("{}", *c);
}
會(huì)出現(xiàn)以下錯(cuò)誤:
error[E0594]: cannot assign to `**c`, which is behind a `&` reference
--> src/main.rs:8:5
|
8 | **c += 1;
| ^^^^^^^^ cannot assign
引用,類似于C/C++中的指針,可以形成任意長(zhǎng)度的復(fù)合類型,這樣,&mut&mut&usize也可以作為Rust引用存在。與指針不同的是,引用的生命周期必須足夠長(zhǎng),否則,借用檢查器會(huì)讓你止步不前。
生命周期
在這里,我們可以探索各種引用的生命周期,并了解何時(shí)創(chuàng)建和銷毀引用(或者像Rust所說的“drop”)。下面的例子:
fn main() {
let mut a = 6;
let mut b = &a;
{
let c = 7;
b = &c;
}
println!("{}", *b);
}
產(chǎn)生錯(cuò)誤:
error[E0597]: `c` does not live long enough
--> src/main.rs:9:13
|
8 | let c = 7;
| - binding `c` declared here
9 | b = &c;
| ^^ borrowed value does not live long enough
10 | }
| - `c` dropped here while still borrowed
11 | println!("{}", *b);
| -- borrow later used here
在內(nèi)部作用域中,b被改變?yōu)楸4鎸?duì)c的引用。但是一旦內(nèi)部作用域結(jié)束,c就不存在了。因此,在這種情況下,引用比它引用的變量生命周期更長(zhǎng),所以產(chǎn)生了錯(cuò)誤。
同樣的規(guī)則不適用于副本,因?yàn)楦北臼潜舜霜?dú)立存在的。如果采用相同的代碼來刪除引用的使用:
fn main() {
let mut a = 6;
let mut b = a;
{
let c = 7;
b = c;
}
println!("{}", b);
}
代碼編譯沒有錯(cuò)誤。由于整數(shù)相對(duì)較小,因此通??梢詮?fù)制它們。然而,更大的類型使用引用計(jì)數(shù)或按引用傳遞,以避免性能下降。
基于作用域的生命周期規(guī)則也適用于在較大的類實(shí)例中獲取引用。
struct Container(Vec<u64>);
impl Container {
fn get(&self, index:usize) -> &u64 {
&self.0[index]
}
}
在上面的代碼中,get返回對(duì)vector中的引用,但是vector的生命周期必須比返回的引用長(zhǎng)。如果我們應(yīng)用同樣的邏輯,
fn main() {
let m = Container(vec![1, 2, 3]);
let mut the_ref = m.get(0);
{
let d = Container(vec![1, 2, 3]);
the_ref = d.get(1);
}
println!("{}", the_ref);
}
此代碼也無法編譯,并出現(xiàn)類似的錯(cuò)誤
error[E0597]: `d` does not live long enough
--> src/main.rs:15:19
|
14 | let d = Container(vec![1, 2, 3]);
| - binding `d` declared here
15 | the_ref = d.get(1);
| ^ borrowed value does not live long enough
16 | }
| - `d` dropped here while still borrowed
17 | println!("{}", the_ref);
| ------- borrow later used here
當(dāng)某些東西在Rust中被刪除時(shí),所有實(shí)現(xiàn)Drop特性的成員也將被刪除。
迭代和引用
當(dāng)在迭代或循環(huán)中使用引用時(shí),有幾種獨(dú)特的行為。如果迭代也是不可變的,則對(duì)集合類型的迭代,通常使循環(huán)充當(dāng)該集合上的不可變借用的作用域。以下代碼為例:
fn main() {
let mut a = vec![1, 2, 3, 4];
for elem in a.iter() {
if *elem % 2 == 0 {
a.remove(*elem);
}
}
}
會(huì)導(dǎo)致編譯錯(cuò)誤:
error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable
--> src/main.rs:8:13
|
6 | for elem in a.iter() {
| --------
| |
| immutable borrow occurs here
| immutable borrow later used here
7 | if *elem % 2 == 0 {
8 | a.remove(*elem);
| ^^^^^^^^^^^^^^^ mutable borrow occurs here
Rust遵循這樣的規(guī)則:對(duì)某種類型的不可變迭代是一系列不可變借用,因此,不能在該迭代期間可變地借用相同的類型。
現(xiàn)在,你可能會(huì)認(rèn)為這段特定代碼的解決方案是對(duì)其進(jìn)行可變迭代。然而,這仍然是不正確的!如果將iter()改為iter_mut():
fn main() {
let mut a = vec![1, 2, 3, 4];
for elem in a.iter_mut() {
if *elem % 2 == 0 {
a.remove(*elem);
}
}
}
會(huì)出現(xiàn)以下錯(cuò)誤:
error[E0499]: cannot borrow `a` as mutable more than once at a time
--> src/main.rs:8:13
|
6 | for elem in a.iter_mut() {
| ------------
| |
| first mutable borrow occurs here
| first borrow later used here
7 | if *elem % 2 == 0 {
8 | a.remove(*elem);
| ^ second mutable borrow occurs here
讓我們回顧一下引用規(guī)則,一個(gè)或多個(gè)不可變引用,或者僅僅是一個(gè)可變引用。在本例中,我們創(chuàng)建了兩個(gè)可變引用,借用檢查器將拒絕它們。但是這個(gè)規(guī)則實(shí)際上是有意義的,它可以保護(hù)免受內(nèi)存損壞錯(cuò)誤的影響。
根據(jù)集合的內(nèi)部實(shí)現(xiàn),修改集合類型會(huì)使現(xiàn)有迭代器失效。這可能是因?yàn)榧咸幚淼膬?nèi)存塊可能被分配或釋放,從而導(dǎo)致懸空指針,但是可變引用規(guī)則有效地防止了這種情況。