OS: Windows 10
Editor: Visual Studio Code
Rust version: 1.63.0
不同語言有著不同的記憶體管理方式,就我所碰過的語言,大多分為兩種:
C
、C++
C#
、Golang
Rust則是由所有權(Ownership) 機制來管理記憶體。
文件對Rust的所有權機制做了簡單的概括:
當擁有者離開作用域時,數值就會被丟棄。
指的作用域是Scope {}
的意思,整段是除了描述所有權外,還包含了生命週期(Life Time) 的概念。例如:
fn main() {
let s = "hello"; // s 誕生
println!("{}", s);
} // s 死亡
或是一個更明顯的例子:
fn main() {
let x = {
let y = 6; // y誕生
y + 1
}; // y死亡
println!("x is {}", x);
}
但這應該不是絕對的,例如許多語言中有static
之類的關鍵字,可以延長或是在運行期間全程存在的作法,留個問題在這邊,之後關注到生命週期的章節再來分析這件事。
首先,試試看這個範例
let x = 5;
let y = x;
println!("x is {}, y is {}", x, y);
// output:
// x is 5, y is 5
如同許多程式語言一樣,基本型別是會進行複製(Copy),但下面就不太一樣了:
let s1 = String::from("hello");
let s2 = s1;
println!("s1 is {}, s2 is {}", s1, s2);
編譯過後,compiler會給錯誤:
Compiling basic v0.1.0 (D:\projects\rust_learning\basic)
error[E0382]: borrow of moved value: `s1`
--> src\main.rs:4:36
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 | println!("s1 is {}, s2 is {}", s1, s2);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
如果有其他語言經驗的話,會知道字串(String
),通常會想要它可以隨意增減長度與內容,所以不太可能直接複製整塊記憶體,畢竟在編譯時也不可能知道完整大小。
而在有GC機制語言的中,s1
跟s2
會同時指向一塊記憶體,直到不再去使用這個字串。
而在這裡,可以很清楚知道Rust中所有權的意思了。原本分配再給s1
的內容,所有權都歸於s2
了,當要再使用s1
的時候就會是無效的了。
當然,也可以依照compiler的指示,使用clone
,複製一整份內容給s2
:
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
接下來,看函式對所有權機制的處理,以下範例是會編譯失敗的:
fn simple_print(s: String) { // s的所有權交給了這裡的函式
println!("{}", s);
} // s離開scope,釋放掉
fn print_len(s: String) {
println!("string length is {}", s.len());
}
fn main() {
let s = String::from("hello");
simple_print(s);
print_len(s); // 錯誤,s已經釋放掉了
}
但這裡就有一個問題,假如之後許多功能,都需要用到某個變數,但在離開作用域(scope)之後就會釋放掉了,這導致如果遵循所有權的規定,我們必須這樣寫:
fn simple_print(s: String) -> String { // s的所有權交給了這裡的函式
println!("{}", s);
s // 用完了,還回去
}
fn print_len(s: String) {
println!("string length is {}", s.len());
}
fn main() {
let s = String::from("hello");
let s = simple_print(s);
print_len(s);
}
這樣就會變得非常不直覺且奇怪。
於是在這裡開始介紹對參數引用(references),
在Rust中,&
這個表示引用,我們修改一下上面的範例:
fn simple_print(s: &String) {
// s的所有權交給了這裡的函式
println!("{}", s);
}
fn print_len(s: &String) {
println!("string length is {}", s.len());
}
fn main() {
let s = String::from("hello");
simple_print(&s);
print_len(&s);
}
我們在函式參數列中,對要引用的參數的型別前面加上&
,並且呼叫函式的時候,對要被引用的參數加上&
表示:「這個變數借給你用(Borrrowing)」。
接下去看下一個範例,假設我要借某個字串,然後修改其中的內容:
fn simple_print(s: &String) {
println!("{}", s);
}
fn print_len(s: &String) {
println!("string length is {}", s.len());
}
fn add_world(s: &String) {
s.push_str(", world");
}
fn main() {
let s = String::from("hello");
add_world(&s);
simple_print(&s);
print_len(&s);
}
編譯之後,給出錯誤
Compiling basic v0.1.0 (D:\projects\rust_learning\basic)
error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
--> src\main.rs:11:5
|
10 | fn add_world(s: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
11 | s.push_str(", world");
| ^^^^^^^^^^^^^^^^^^^^^ `s` is a `&` reference, so the data it refers to cannot be borrowed as mutable
For more information about this error, try `rustc --explain E0596`.
編譯器建議我們加上mut
,而這個就是可變引用。
fn simple_print(s: &String) {
println!("{}", s);
}
fn print_len(s: &String) {
println!("string length is {}", s.len());
}
fn add_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut s = String::from("hello"); // 記得這裡要改為mut
add_world(&mut s);
simple_print(&s);
print_len(&s);
}
這樣compiler就不會跟我們抱怨了。
假如是引用的話(不去動原始資料的那種),是可以一次借給多個人的
// 這個編譯會過
let s = String::from("hello");
let x = &s;
let y = &s;
println!("x is {}, y is {}", x, y);
但如果是可變引用的話,是不可以的
let mut s = String::from("hello");
let x = &mut s;
let y = &mut s;
可變引用一次限制一個人使用,這個限制很大程度在編譯期就預防了資料競爭(data races),既同一時間不同地方寫入同一個變數。
但如果限制作用域,是可行的,因為在別的作用域結束之後,變數的借用就結束了:
fn simple_print(s: &String) {
println!("{}", s);
}
fn add_world(s: &mut String) {
s.push_str(", world");
}
fn add_exclamation_mark(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut s = String::from("hello");
add_world(&mut s);
add_exclamation_mark(&mut s);
simple_print(&s);
}
再來也不可以同時對同一個變數作引用跟可變引用,因為這有可能讓引用突然被借去改變了值
let mut s = String::from("hello");
let s1 = &s;
let s2 = &s;
let s3 = &mut s;
println!("{}, {} and {}", s1, s2, s3);
但也是一樣,直到借出去的被借完之後,在換人借就可以了
let mut s = String::from("hello");
let s1 = &s;
let s2 = &s;
println!("{}, {}", s1, s2); // s 被 s1, s2借去用了
// s1 跟 s2不再借用s了
let s3 = &mut s; // s3 借 s 拿去用
println!("{}", s3);