4.4.0 寫在正文之前
這一節的內容其實就相當于C++的智能指針移動語義在編譯器層面做了一些約束。Rust中引用的寫法通過編譯器的約束寫成了C++中最理想、最規范的指針寫法。所以學過C++的人對這一章肯定會非常熟悉。
喜歡的話別忘了點贊、收藏加關注哦(加關注即可閱讀全文),對接下來的教程有興趣的可以關注專欄。謝謝喵!(=・ω・=)
4.4.1. 引用
引用讓函數使用某個值而不獲得其所有權,聲明時在類型前加上&
即代表引用。例如String
的引用就是&String
。如果學過C++的話,C++中的解引用符號是*
,Rust中也是一樣的。
學了引用之后,就可以把上一篇文章最后的示例代碼給簡化
這是先前的代碼:
fn main(){let s1 = String::from("hello");let (s2, len) = calculate_length(s1);println!("The length of '{}' is {}", s2, len);
}fn calculate_length(s:String) -> (String, uszie) {let length = s.len();(s, length)
}
這是修改后的代碼:
fn main(){let s1 = String::from("hello");let length = calculate_length(&s1);println!("The length of '{}' is {}", s1, length);
}fn calculate_length(s:&String) -> usize {s.len()
}
對比兩者,后者中數據的指針被傳入函數calculate_length
供其操作,而數據所有權依然在變量s1
上。不需要返回元組,也不需要再聲明一個變量s2
,更加簡潔。
函數calculate_length
的參數s
實際上是一個指針,指向s
所在棧內存位置(不會直接指向堆內存中的數據)。這個指針在走出作用域時,Rust并不會消除其指向的數據(因為s
沒有所有權),只會彈出棧上所存儲的指針信息,也就是釋放下圖中的最左側的部分所占的內存。
這種以引用作為函數的參數叫做借用
4.4.2. 借用的特性
借用的內容是不能被修改的,除非是可變引用
以房產為例:你把自己有房產權的房子租給別人就是借用,租戶只能住不能亂裝修,這就是借用的內容不能被修改的特性;如果你允許租客裝修,這就是可變引用。
以這個代碼為例:
fn main(){let s1 = String::from("hello");let length = calculate_length(&s1);println!("The length of '{}' is {}", s1, length);
}fn calculate_length(s:&String) -> usize {s.push_str(", world");s.len()
}
在編譯時這個代碼會報錯:
error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
報錯的原因在于s.push_str(", world");
這一行:引用默認是不可變的,但這一行修改了其數據內容。
引用跟普通的變量聲明一樣,默認不可變,但加上mut
關鍵字后就可變了:
fn main(){let mut s1 = String::from("hello");let length = calculate_length(&mut s1);println!("The length of '{}' is {}", s1, length);
}fn calculate_length(s:&mut String) -> usize {s.push_str(", world");s.len()
}
這樣寫就不會報錯了(但記得在聲明s1
時把s1
聲明為可變變量)
這種可以修改數據內容的引用就叫做可變引用
4.4.3. 可變引用的限制
可變引用有兩個非常重要的限制,其一是:在特定作用域內,對某一塊數據,只能有一個可變的引用。
以這個代碼為例:
fn main() {let mut s = String::from("hello");let s1 = &mut s;let s2 = &mut s;
}
因為s1
和s2
都是指向s
的可變引用,且在同一個作用域內,所以在編譯時會報錯:
error[E0499]: cannot borrow `s` as mutable more than once at a time
這么做的目的是防止數據競爭,以下三種條件同時滿足時會發生數據競爭:
- 兩個或多個指針同時訪問同一個數據
- 至少有一個指針用于寫入數據
- 沒有使用任何機制來同步對數據的訪問
在報錯信息中提及了at a time
,意思為同時(也就是在同一個作用域內)。所以說,只要不同時,也就是兩個可變引用在不同的作用域指向同一塊數據是可以的。下面的代碼就體現了這一點:
fn main() {let mut s = String::from("hello");{let s1 = &mut s;}let s2 = &mut s;
}
s1
和s2
作用域不相同,所以指向同一塊數據是允許的。
可變引用的第二個重要限制是:不可以同時擁有一個可變引用和一個不變的引用。 因為可變引用存在的目的是修改數據內容,不變的引用存在的作用就是為了保持數據內容不變,如果兩者同時存在,可變引用修改值之后,不可變引用的作用就失效了。
fn main() {let mut s = String::from("hello");let s1 = &mut s;let s2 = &s;
}
因為s1
是可變引用,s2
是不可變引用,兩者出現在同一個作用域指向同一塊數據,所以編譯器會報錯:
error[E0502]: cannot borrow `s` as mutable because it also borrowed as immutable
當然,多個不可變的引用是可以同時出現的。
總結:多個讀(不可變引用)是可以同時存在的,多個寫(可變引用)可以存在但不能同時,多個寫和同時讀寫是不允許的。
4.4.4. 懸空引用(Dangling References)
在使用指針時非常容易引起叫做懸空指針(Dangling Pointer) 的錯誤,其定義為:一個指針引用了內存中的某個地址,而這塊內存可能已經釋放并分配給其他人使用了。
如果你引用了某些數據,Rust編譯器保證在引用離開作用域前數據不會離開作用域。 這是Rust保證懸空引用永遠不會出現的做法。
以這個代碼為例:
fn main() {let r = dangle();
}fn dangle() -> &String {let s = String::from("hello");&s
}
- 創建了一個局部變量 s:
變量s
是一個String
,它被分配在棧上,但其底層數據存儲在堆上。 - 返回對
s
的引用:
函數最后通過&s
返回了s
的引用。 - s 的作用域結束:
在函數dangle
返回后,變量s
離開了作用域,根據Rust所有權規則,s
的內存被自動釋放,&s
所指向的內存數據已不再存儲s
的數據,返回的引用指向的是已經被釋放的內存地址,變成了懸空引用(Dangling Pointer)。
Rust的編譯器會檢查到這一點,在編譯時會報錯。
4.4.5. 引用的規則
- 在任何給定的時刻,只能滿足下列條件之一:
- 一個可變的引用
- 任意數量不可變的引用
- 引用必須一直有效