Rust 基礎系列 #2: 在 Rust 程序中使用變數和常量
在 該系列的第一章中,我講述了為什麼 Rust 是一門越來越流行的編程語言。我還展示了如何 在 Rust 中編寫 Hello World 程序。
讓我們繼續 Rust 之旅。在本文中,我將向你介紹 Rust 編程語言中的變數和常量。
此外,我還將講解一個稱為「 遮蔽 」的新編程概念。
Rust 變數的獨特之處
在編程語言中,變數是指 存儲某些數據的內存地址的一個別名 。
對 Rust 語言來講也是如此。但是 Rust 有一個獨特的「特性」。每個你聲明的變數都是 默認 不可變的 。這意味著一旦給變數賦值,就不能再改變它的值。
這個決定是為了確保默認情況下,你不需要使用 自旋鎖 或 互斥鎖 等特殊機制來引入多線程。Rust 會保證 安全的並發。由於所有變數(默認情況下)都是不可變的,因此你不需要擔心線程會無意中更改變數值。
這並不是在說 Rust 中的變數就像常量一樣,因為它們確實不是常量。變數可以被顯式地定義為可變的。這樣的變數稱為 可變變數 。
這是在 Rust 中聲明變數的語法:
// 默認情況下不可變
// 初始化值是**唯一**的值
let variable_name = value;
// 使用 'mut' 關鍵字定義可變變數
// 初始化值可以被改變
let mut variable_name = value;
? 儘管你可以改變可變變數的值,但你不能將另一種數據類型的值賦值給它。
這意味著,如果你有一個可變的浮點型變數,你不能在後面將一個字元賦值給它。
Rust 數據類型概觀
在上一篇文章中,你可能注意到了我提到 Rust 是一種強類型語言。但是在定義變數時,你不需要指定數據類型,而是使用一個通用的關鍵字 let
。
Rust 編譯器可以根據賦值給變數的值推斷出變數的數據類型。但是如果你仍然希望明確指定數據類型並希望注釋類型,那麼可以這樣做。以下是語法:
let variable_name: data_type = value;
下面是 Rust 編程語言中一些常見的數據類型:
- 整數類型:分別用於有符號和無符號的 32 位整數的
i32
和u32
- 浮點類型:分別用於 32 位和 64 位浮點數的
f32
和f64
- 布爾類型:
bool
- 字元類型:
char
我會在下一篇文章中更詳細地介紹 Rust 的數據類型。現在,這應該足夠了。
? Rust 並不支持隱式類型轉換。因此,如果你將值
8
賦給一個浮點型變數,你將會遇到編譯時錯誤。你應該賦的值是8.
或8.0
。
Rust 還強制要求在讀取存儲在其中的值之前初始化變數。
{ // 該代碼塊不會被編譯
let a;
println!("{}", a); // 本行報錯
// 讀取一個**未初始化**變數的值是一個編譯時錯誤
}
{ // 該代碼塊會被編譯
let a;
a = 128;
println!("{}", a); // 本行不會報錯
// 變數 'a' 有一個初始值
}
如果你在不初始化的情況下聲明一個變數,並在給它賦值之前使用它,Rust 編譯器將會拋出一個 編譯時錯誤 。
雖然錯誤很煩人,但在這種情況下,Rust 編譯器強制你不要犯寫代碼時常見的錯誤之一:未初始化的變數。
Rust 編譯器的錯誤信息
來寫幾個程序,你將
- 通過執行「正常」的任務來理解 Rust 的設計,這些任務實際上是內存相關問題的主要原因
- 閱讀和理解 Rust 編譯器的錯誤/警告信息
測試變數的不可變性
讓我們故意寫一個試圖修改不可變變數的程序,看看接下來會發生什麼。
fn main() {
let mut a = 172;
let b = 273;
println!("a: {a}, b: {b}");
a = 380;
b = 420;
println!("a: {}, b: {}", a, b);
}
直到第 4 行看起來都是一個簡單的程序。但是在第 7 行,變數 b
—— 一個不可變變數 —— 的值被修改了。
注意列印 Rust 變數值的兩種方法。在第 4 行,我將變數括在花括弧中,以便列印它們的值。在第 8 行,我保持括弧為空,並使用 C 的風格將變數作為參數。這兩種方法都是有效的。(除了修改不可變變數的值,這個程序中的所有內容都是正確的。)
來編譯一下!如果你按照上一章的步驟做了,你已經知道該怎麼做了。
$ rustc main.rs
error[E0384]: cannot assign twice to immutable variable `b`
--> main.rs:7:5
|
3 | let b = 273;
| -
| |
| first assignment to `b`
| help: consider making this binding mutable: `mut b`
...
7 | b = 420;
| ^^^^^^^ cannot assign twice to immutable variable
error: aborting due to previous error
For more information about this error, try `rustc --explain E0384`.
? 「binding」 一詞是指變數名。但這只是一個簡單的解釋。
這很好的展示了 Rust 強大的錯誤檢查和信息豐富的錯誤信息。第一行展示了阻止上述代碼編譯的錯誤信息:
error[E0384]: cannot assign twice to immutable variable b
這意味著,Rust 編譯器注意到我試圖給變數 b
重新賦值,但變數 b
是一個不可變變數。所以這就是導致這個錯誤的原因。
編譯器甚至可以識別出錯誤發生的確切行和列號。
在顯示 first assignment to b
的行下面,是提供幫助的行。因為我正在改變不可變變數 b
的值,所以我被告知使用 mut
關鍵字將變數 b
聲明為可變變數。
?️ 自己實現一個修復來更好地理解手頭的問題。
使用未初始化的變數
現在,讓我們看看當我們嘗試讀取未初始化變數的值時,Rust 編譯器會做什麼。
fn main() {
let a: i32;
a = 123;
println!("a: {a}");
let b: i32;
println!("b: {b}");
b = 123;
}
這裡,我有兩個不可變變數 a
和 b
,在聲明時都沒有初始化。變數 a
在其值被讀取之前被賦予了一個值。但是變數 b
的值在被賦予初始值之前被讀取了。
來編譯一下,看看結果。
$ rustc main.rs
warning: value assigned to `b` is never read
--> main.rs:8:5
|
8 | b = 123;
| ^
|
= help: maybe it is overwritten before being read?
= note: `#[warn(unused_assignments)]` on by default
error[E0381]: used binding `b` is possibly-uninitialized
--> main.rs:7:19
|
6 | let b: i32;
| - binding declared here but left uninitialized
7 | println!("b: {b}");
| ^ `b` used here but it is possibly-uninitialized
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to previous error; 1 warning emitted
For more information about this error, try `rustc --explain E0381`.
這裡,Rust 編譯器拋出了一個編譯時錯誤和一個警告。警告說變數 b
的值從來沒有被讀取過。
但是這是荒謬的!變數 b
的值在第 7 行被訪問了。但是仔細看;警告是關於第 8 行的。這很令人困惑;讓我們暫時跳過這個警告,繼續看錯誤。
這個錯誤信息說 used binding b is possibly-uninitialized
。和之前的例子一樣,Rust 編譯器指出錯誤是由於嘗試在第 7 行讀取變數 b
的值而引起的。讀取變數 b
的值是錯誤的原因是它的值沒有初始化。在 Rust 編程語言中,這是非法的。因此編譯時錯誤出現。
?️ 這個錯誤可以很容易地通過交換第 7 和第 8 行的代碼來解決。試一下,看看錯誤是否消失了。
示常式序:交換數字
現在你已經熟悉了常見的變數相關問題,讓我們來看一個交換兩個變數值的程序。
fn main() {
let mut a = 7186932;
let mut b = 1276561;
println!("a: {a}, b: {b}");
// 交換變數值
let temp = a;
a = b;
b = temp;
println!("a: {}, b: {}", a, b);
}
我在這裡聲明了兩個變數 a
和 b
。這兩個變數都是可變的,因為我希望在後面改變它們的值。我賦予了一些隨機值。最初,我列印了這些變數的值。
然後,在第 8 行,我創建了一個名為 temp
的不可變變數,並將存儲在 a
中的值賦給它。之所以這個變數是不可變的,是因為 temp
的值不會改變。
要交換值,我將變數 b
的值賦給變數 a
,在下一行,我將 temp
的值(它包含 a
的值)賦給變數 b
。現在值已經交換了,我列印了變數 a
和 b
的值。
在編譯並執行上面的代碼後,我得到了以下輸出:
a: 7186932, b: 1276561
a: 1276561, b: 7186932
正如你所見,值已經交換了。完美。
使用未使用的變數
當你聲明了一些變數,打算在後面使用它們,但是還沒有使用它們,然後編譯你的 Rust 代碼來檢查一些東西時,Rust 編譯器會警告你。
原因是顯而易見的。不會被使用的變數佔用了不必要的初始化時間(CPU 周期)和內存空間。如果不會被使用,為什麼要在程序寫上它呢?儘管編譯器確實會優化這一點。但是它仍然是一個問題,因為它會以多餘的代碼的形式影響可讀性。
但是,有的時候,你可能會面對這樣的情況:創建一個變數與否不在你的控制之下。比如說,當一個函數返回多個值,而你只需要其中的一些值時。在這種情況下,你不能要求庫維護者根據你的需要調整他們的函數。
所以,在這種情況下,你可以寫一個以下劃線開頭的變數,Rust 編譯器將不再顯示這樣的警告。如果你真的不需要使用存儲在該未使用變數中的值,你可以簡單地將其命名為 _
(下劃線),Rust 編譯器也會忽略它!
接下來的程序不僅不會生成任何輸出,而且也不會生成任何警告和/或錯誤消息:
fn main() {
let _unnecessary_var = 0; // 沒有警告
let _ = 0.0; // 完全忽略
}
算術運算
數學就是數學,Rust 並沒有在這方面創新。你可以使用在其他編程語言(如 C、C++ 和/或 Java)中使用過的所有算術運算符。
包含可以在 Rust 編程語言中使用的所有運算符和它們的含義的完整列表可以在 這裡 找到。
示常式序:一個生鏽的溫度計
(LCTT 譯註:這裡的溫度計「生鏽」了是因為它是使用 Rust(生鏽)編寫的,原作者在這裡玩了一個雙關。)
接下來是一個典型的程序,它將華氏度轉換為攝氏度,反之亦然。
fn main() {
let boiling_water_f: f64 = 212.0;
let frozen_water_c: f64 = 0.0;
let boiling_water_c = (boiling_water_f - 32.0) * (5.0 / 9.0);
let frozen_water_f = (frozen_water_c * (9.0 / 5.0)) + 32.0;
println!(
"Water starts boiling at {}°C (or {}°F).",
boiling_water_c, boiling_water_f
);
println!(
"Water starts freezing at {}°C (or {}°F).",
frozen_water_c, frozen_water_f
);
}
沒什麼大不了的……華氏溫度轉換為攝氏溫度,反之亦然。
正如你在這裡看到的,由於 Rust 不允許自動類型轉換,我不得不在整數 32、9 和 5 後放一個小數點。除此之外,這與你在 C、C++ 和/或 Java 中所做的類似。
作為練習,嘗試編寫一個程序,找出給定數中有多少位數字。
常量
如果你有一些編程知識,你可能知道這意味著什麼。常量是一種特殊類型的變數,它的值永遠不會改變。它保持不變。
在 Rust 編程語言中,使用以下語法聲明常量:
const CONSTANT_NAME: data_type = value;
如你所見,聲明常量的語法與我們在 Rust 中看到的變數聲明非常相似。但是有兩個不同之處:
- 常量的名字需要像
SCREAMING_SNAKE_CASE
這樣。所有的大寫字母和單詞之間用下劃線分隔。 - 常量的數據類型必須被顯性定義。
變數與常量的對比
你可能在想,既然變數默認是不可變的,為什麼語言還要包含常量呢?
接下來這個表格應該可以幫助你消除疑慮。(如果你好奇並且想更好地理解這些區別,你可以看看我的博客,它詳細地展示了這些區別。)
使用常量的示常式序:計算圓的面積
這是一個很直接的關於 Rust 中常量的簡單程序。它計算圓的面積和周長。
fn main() {
const PI: f64 = 3.14;
let radius: f64 = 50.0;
let circle_area = PI * (radius * radius);
let circle_perimeter = 2.0 * PI * radius;
println!("有一個周長為 {radius} 厘米的圓");
println!("它的面積是 {} 平方厘米", circle_area);
println!(
"以及它的周長是 {} 厘米",
circle_perimeter
);
}
如果運行代碼,將產生以下輸出:
有一個周長為 50 厘米的圓
它的面積是 7850 平方厘米
以及它的周長是 314 厘米
Rust 中的變數遮蔽
如果你是一個 C++ 程序員,你可能已經知道我在說什麼了。當程序員聲明一個與已經聲明的變數同名的新變數時,這就是變數遮蔽。
與 C++ 不同,Rust 允許你在同一作用域中執行變數遮蔽!
? 當程序員遮蔽一個已經存在的變數時,新變數會被分配一個新的內存地址,但是使用與現有變數相同的名稱引用。
來看看它在 Rust 中是如何工作的。
fn main() {
let a = 108;
println!("a 的地址: {:p}, a 的值 {a}", &a);
let a = 56;
println!("a 的地址: {:p}, a 的值: {a} // 遮蔽後", &a);
let mut b = 82;
println!("nb 的地址: {:p}, b 的值: {b}", &b);
let mut b = 120;
println!("b的地址: {:p}, b的值: {b} // 遮蔽後", &b);
let mut c = 18;
println!("nc 的地址: {:p}, c的值: {c}", &c);
c = 29;
println!("c 的地址: {:p}, c的值: {c} // 遮蔽後", &c);
}
println
語句中花括弧內的 :p
與 C 中的 %p
類似。它指定值的格式為內存地址(指針)。
我在這裡使用了 3 個變數。變數 a
是不可變的,並且在第 4 行被遮蔽。變數 b
是可變的,並且在第 9 行也被遮蔽。變數 c
是可變的,但是在第 14 行,只有它的值被改變了。它沒有被遮蔽。
現在,讓我們看看輸出。
a 的地址: 0x7ffe954bf614, a 的值 108
a 的地址: 0x7ffe954bf674, a 的值: 56 // 遮蔽後
b 的地址: 0x7ffe954bf6d4, b 的值: 82
b 的地址: 0x7ffe954bf734, b 的值: 120 // 遮蔽後
c 的地址: 0x7ffe954bf734, c 的值: 18
c 的地址: 0x7ffe954bf734, c 的值: 29 // 遮蔽後
來看看輸出,你會發現不僅所有三個變數的值都改變了,而且被遮蔽的變數的地址也不同(檢查十六進位的最後幾個字元)。
變數 a
和 b
的內存地址改變了。這意味著變數的可變性或不可變性並不是遮蔽變數的限制。
總結
本文介紹了 Rust 編程語言中的變數和常量。還介紹了算術運算。
做個總結:
- Rust 中的變數默認是不可變的,但是可以引入可變性。
- 程序員需要顯式地指定變數的可變性。
- 常量總是不可變的,無論如何都需要類型注釋。
- 變數遮蔽是指使用與現有變數相同的名稱聲明一個 新 變數。
很好!我相信和 Rust 一起的進展不錯。在下一章中,我將討論 Rust 中的數據類型。敬請關注。
與此同時,如果你有任何問題,請告訴我。
(題圖:MJ/7c5366b8-f926-487e-9153-0a877145ca5)
via: https://itsfoss.com/rust-variables/
作者:Pratham Patel 選題:lkxed 譯者:Cubik 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive