參考與借用
在清單4-5中的元組代碼的問題在于,我們必須將String
返回給調用函數,這樣我們才能在調用calculate_length
之后繼續使用String
,因為String
已經被移動到了calculate_length
中。相反,我們可以提供一個對String
值的引用。引用類似于指針,它是一個地址,我們可以沿著它訪問存儲在該地址的數據;這些數據由其他某個變量擁有。與指針不同的是,引用在引用的生命周期內保證指向一個特定類型的有效值。
以下是如何定義和使用一個以對象的引用作為參數而不是獲取值的所有權的calculate_length
函數:
文件名:src/main.rs
fn main() {let s1 = String::from("hello");let len = calculate_length(&s1);println!("'{}'的長度是{}。", s1, len);
}
fn calculate_length(s: &String) -> usize {s.len()
}
首先,注意變量聲明和函數返回值中的所有元組代碼都消失了。其次,注意我們將&s1
傳遞給calculate_length
,在其定義中,我們接受&String
而不是String
。這些&
符號代表引用,它們允許你在不獲取所有權的情況下引用某個值。圖4-6展示了這一概念。
圖4-6:一個指向String s1
的&String s
的示意圖
注意:使用&
來引用的相反操作是解引用,這可以通過解引用運算符*
來完成。我們將在第8章看到一些解引用運算符的用法,并在第15章詳細討論解引用的細節。
讓我們更仔細地看看這里的函數調用:
let s1 = String::from("hello");let len = calculate_length(&s1);
&s1
的語法讓我們創建了一個引用,它指向s1
的值,但并不擁有它。因為引用并不擁有它,所以當引用不再被使用時,它所指向的值不會被釋放。
同樣,函數的簽名使用&
來表明參數s
的類型是一個引用。我們來添加一些解釋性的注釋:
fn calculate_length(s: &String) -> usize { // s是一個指向String的引用s.len()
} // 在這里,s的作用域結束了。但由于s并不擁有它所引用的內容,所以值不會被釋放。
變量s
的有效作用域與任何函數參數的作用域相同,但引用所指向的值在s
不再被使用時不會被釋放,因為s
并不擁有它。當函數的參數是引用而不是實際值時,我們不需要返回值以交還所有權,因為我們從未擁有過所有權。
我們將創建引用的行為稱為借用。就像在現實生活中一樣,如果一個人擁有某樣東西,你可以從他那里借來。當你用完后,你必須歸還。你并不擁有它。
那么,如果我們嘗試修改我們正在借用的內容會發生什么呢?嘗試清單4-6中的代碼。劇透警告:它無法工作!
文件名:src/main.rs
這段代碼無法編譯!
fn main() {let s = String::from("hello");change(&s);
}fn change(some_string: &String) {some_string.push_str(", world");
}
清單4-6:嘗試修改一個被借用的值
這是錯誤信息:
$ cargo runCompiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference--> src/main.rs:8:5|
8 | some_string.push_str(", world");| ^^^^^^^^^^^ `some_string`是一個`&`引用,因此它所引用的數據不能被借用為可變的|
help: 考慮將其改為可變引用|
7 | fn change(some_string: &mut String) {| +++
有關此錯誤的更多信息,請嘗試rustc --explain E0596
。
錯誤:由于之前的1個錯誤,無法編譯ownership
(二進制文件“ownership”)
正如變量默認是不可變的一樣,引用也是不可變的。我們不允許修改我們引用的內容。
可變引用
我們可以通過一些小的調整來修復清單4-6中的代碼,從而允許我們修改一個被借用的值。這些調整使用的是可變引用:
文件名:src/main.rs
fn main() {let mut s = String::from("hello");change(&mut s);
}fn change(some_string: &mut String) {some_string.push_str(", world");
}
首先,我們將s
改為mut
。然后,我們在調用change
函數時使用&mut s
創建一個可變引用,并更新函數簽名以接受一個可變引用some_string: &mut String
。這使得change
函數將修改它借用的值這一行為變得非常清晰。
可變引用有一個重要的限制:如果你有一個對某個值的可變引用,那么你不能有其他對該值的引用。這段嘗試為s
創建兩個可變引用的代碼將無法通過編譯:
文件名:src/main.rs
這段代碼無法編譯!
let mut s = String::from("hello");let r1 = &mut s;
let r2 = &mut s;println!("{}, {}", r1, r2);
這是錯誤信息:
$ cargo runCompiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time--> src/main.rs:5:14|
4 | let r1 = &mut s;| ------ 第一個可變借用發生在這里
5 | let r2 = &mut s;| ^^^^^^ 第二個可變借用發生在這里
6 |
7 | println!("{}, {}", r1, r2);| -- 第一個借用在此處后續使用有關此錯誤的更多信息,請嘗試`rustc --explain E0499`。
錯誤:由于之前的1個錯誤,無法編譯`ownership`(二進制文件“ownership”)
這個錯誤表明這段代碼是無效的,因為我們不能同時多次將s
借用為可變的。第一個可變借用在r1
中,必須持續到它在println!
中被使用為止,但在那個可變引用創建和使用之間,我們試圖在r2
中創建另一個可變引用,它與r1
借用相同的數據。
防止同時對相同數據進行多次可變引用的限制允許進行修改,但這種修改是受到嚴格控制的。新Rustaceans(Rust新手)常常會在這個問題上掙扎,因為大多數語言允許你在任何時候進行修改。這種限制的好處是Rust可以在編譯時防止數據競爭。數據競爭類似于競態條件,當出現以下三種行為時就會發生:
- 兩個或更多的指針同時訪問相同的數據。
- 至少有一個指針被用來寫入數據。
- 沒有機制被用來同步對數據的訪問。
數據競爭會導致未定義行為,并且在運行時試圖追蹤它們時很難診斷和修復;Rust通過拒絕編譯存在數據競爭的代碼來防止這個問題!
正如我們總是可以使用大括號來創建一個新作用域一樣,我們可以允許有多個可變引用,只要它們不是同時存在的即可:
let mut s = String::from("hello");{let r1 = &mut s;
} // r1在這里結束作用域,因此我們可以毫無問題地創建一個新的引用。let r2 = &mut s;
Rust對組合可變引用和不可變引用也執行類似的規則。這段代碼會導致錯誤:
這段代碼無法編譯!
let mut s = String::from("hello");let r1 = &s; // 沒有問題
let r2 = &s; // 沒有問題
let r3 = &mut s; // 大問題println!("{}, {}, and {}", r1, r2, r3);
這是錯誤信息:
$ cargo runCompiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable--> src/main.rs:6:14|
4 | let r1 = &s; // 沒有問題| -- 不可變借用發生在這里
5 | let r2 = &s; // 沒有問題
6 | let r3 = &mut s; // 大問題| ^^^^^^ 可變借用發生在這里
7 |
8 | println!("{}, {}, and {}", r1, r2, r3);| -- 不可變借用在此處后續使用有關此錯誤的更多信息,請嘗試`rustc --explain E0502`。
錯誤:由于之前的1個錯誤,無法編譯`ownership`(二進制文件“ownership”)
呼!我們也不能在有對同一個值的不可變引用的同時擁有一個可變引用。
不可變引用的使用者不會期望值在他們不知情的情況下突然改變!然而,允許多個不可變引用是合理的,因為那些只是讀取數據的人無法影響其他人的讀取。
注意,引用的作用域從它被引入的地方開始,并持續到最后一次使用該引用的地方。例如,這段代碼可以編譯,因為不可變引用的最后一次使用是在println!
中,這在可變引用被引入之前:
let mut s = String::from("hello");let r1 = &s; // 沒有問題
let r2 = &s; // 沒有問題
println!("{r1} and {r2}");
// 變量r1和r2在此點之后將不再被使用。let r3 = &mut s; // 沒有問題
println!("{r3}");
不可變引用r1
和r2
的作用域在它們最后一次被使用的println!
之后結束,這在可變引用r3
被創建之前。這些作用域沒有重疊,所以這段代碼是被允許的:編譯器可以判斷出在作用域結束之前引用不再被使用。
接下來,我們將看看另一種引用類型:切片。