Rust 基礎(chǔ)系列 #2: 在 Rust 程序中使用變量和常量
在 該系列的第一章中,我講述了為什么 Rust 是一門越來(lái)越流行的編程語(yǔ)言。我還展示了如何 在 Rust 中編寫 Hello World 程序。
讓我們繼續(xù) Rust 之旅。在本文中,我將向你介紹 Rust 編程語(yǔ)言中的變量和常量。
此外,我還將講解一個(gè)稱為“遮蔽shadowing”的新編程概念。
Rust 變量的獨(dú)特之處
在編程語(yǔ)言中,變量是指 存儲(chǔ)某些數(shù)據(jù)的內(nèi)存地址的一個(gè)別名 。
對(duì) Rust 語(yǔ)言來(lái)講也是如此。但是 Rust 有一個(gè)獨(dú)特的“特性”。每個(gè)你聲明的變量都是 默認(rèn) 不可變的immutable 。這意味著一旦給變量賦值,就不能再改變它的值。
這個(gè)決定是為了確保默認(rèn)情況下,你不需要使用 自旋鎖spin lock 或 互斥鎖mutex 等特殊機(jī)制來(lái)引入多線程。Rust 會(huì)保證 安全的并發(fā)。由于所有變量(默認(rèn)情況下)都是不可變的,因此你不需要擔(dān)心線程會(huì)無(wú)意中更改變量值。
這并不是在說(shuō) Rust 中的變量就像常量一樣,因?yàn)樗鼈兇_實(shí)不是常量。變量可以被顯式地定義為可變的。這樣的變量稱為 可變變量 。
這是在 Rust 中聲明變量的語(yǔ)法:
// 默認(rèn)情況下不可變
// 初始化值是**唯一**的值
let variable_name = value;
// 使用 'mut' 關(guān)鍵字定義可變變量
// 初始化值可以被改變
let mut variable_name = value;
?? 盡管你可以改變可變變量的值,但你不能將另一種數(shù)據(jù)類型的值賦值給它。
這意味著,如果你有一個(gè)可變的浮點(diǎn)型變量,你不能在后面將一個(gè)字符賦值給它。
Rust 數(shù)據(jù)類型概觀
在上一篇文章中,你可能注意到了我提到 Rust 是一種強(qiáng)類型語(yǔ)言。但是在定義變量時(shí),你不需要指定數(shù)據(jù)類型,而是使用一個(gè)通用的關(guān)鍵字 let
。
Rust 編譯器可以根據(jù)賦值給變量的值推斷出變量的數(shù)據(jù)類型。但是如果你仍然希望明確指定數(shù)據(jù)類型并希望注釋類型,那么可以這樣做。以下是語(yǔ)法:
let variable_name: data_type = value;
下面是 Rust 編程語(yǔ)言中一些常見(jiàn)的數(shù)據(jù)類型:
- 整數(shù)類型:分別用于有符號(hào)和無(wú)符號(hào)的 32 位整數(shù)的
i32
和u32
- 浮點(diǎn)類型:分別用于 32 位和 64 位浮點(diǎn)數(shù)的
f32
和f64
- 布爾類型:
bool
- 字符類型:
char
我會(huì)在下一篇文章中更詳細(xì)地介紹 Rust 的數(shù)據(jù)類型?,F(xiàn)在,這應(yīng)該足夠了。
?? Rust 并不支持隱式類型轉(zhuǎn)換。因此,如果你將值
8
賦給一個(gè)浮點(diǎn)型變量,你將會(huì)遇到編譯時(shí)錯(cuò)誤。你應(yīng)該賦的值是8.
或8.0
。
Rust 還強(qiáng)制要求在讀取存儲(chǔ)在其中的值之前初始化變量。
{ // 該代碼塊不會(huì)被編譯
let a;
println!("{}", a); // 本行報(bào)錯(cuò)
// 讀取一個(gè)**未初始化**變量的值是一個(gè)編譯時(shí)錯(cuò)誤
}
{ // 該代碼塊會(huì)被編譯
let a;
a = 128;
println!("{}", a); // 本行不會(huì)報(bào)錯(cuò)
// 變量 'a' 有一個(gè)初始值
}
如果你在不初始化的情況下聲明一個(gè)變量,并在給它賦值之前使用它,Rust 編譯器將會(huì)拋出一個(gè) 編譯時(shí)錯(cuò)誤 。
雖然錯(cuò)誤很煩人,但在這種情況下,Rust 編譯器強(qiáng)制你不要犯寫代碼時(shí)常見(jiàn)的錯(cuò)誤之一:未初始化的變量。
Rust 編譯器的錯(cuò)誤信息
來(lái)寫幾個(gè)程序,你將
- 通過(guò)執(zhí)行“正常”的任務(wù)來(lái)理解 Rust 的設(shè)計(jì),這些任務(wù)實(shí)際上是內(nèi)存相關(guān)問(wèn)題的主要原因
- 閱讀和理解 Rust 編譯器的錯(cuò)誤/警告信息
測(cè)試變量的不可變性
讓我們故意寫一個(gè)試圖修改不可變變量的程序,看看接下來(lái)會(huì)發(fā)生什么。
fn main() {
let mut a = 172;
let b = 273;
println!("a: {a}, b: ");
a = 380;
b = 420;
println!("a: {}, b: {}", a, b);
}
直到第 4 行看起來(lái)都是一個(gè)簡(jiǎn)單的程序。但是在第 7 行,變量 b
—— 一個(gè)不可變變量 —— 的值被修改了。
注意打印 Rust 變量值的兩種方法。在第 4 行,我將變量括在花括號(hào)中,以便打印它們的值。在第 8 行,我保持括號(hào)為空,并使用 C 的風(fēng)格將變量作為參數(shù)。這兩種方法都是有效的。(除了修改不可變變量的值,這個(gè)程序中的所有內(nèi)容都是正確的。)
來(lái)編譯一下!如果你按照上一章的步驟做了,你已經(jīng)知道該怎么做了。
$ 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” 一詞是指變量名。但這只是一個(gè)簡(jiǎn)單的解釋。
這很好的展示了 Rust 強(qiáng)大的錯(cuò)誤檢查和信息豐富的錯(cuò)誤信息。第一行展示了阻止上述代碼編譯的錯(cuò)誤信息:
error[E0384]: cannot assign twice to immutable variable b
這意味著,Rust 編譯器注意到我試圖給變量 b
重新賦值,但變量 b
是一個(gè)不可變變量。所以這就是導(dǎo)致這個(gè)錯(cuò)誤的原因。
編譯器甚至可以識(shí)別出錯(cuò)誤發(fā)生的確切行和列號(hào)。
在顯示 first assignment to b
的行下面,是提供幫助的行。因?yàn)槲艺诟淖儾豢勺冏兞?nbsp;b
的值,所以我被告知使用 mut
關(guān)鍵字將變量 b
聲明為可變變量。
??? 自己實(shí)現(xiàn)一個(gè)修復(fù)來(lái)更好地理解手頭的問(wèn)題。
使用未初始化的變量
現(xiàn)在,讓我們看看當(dāng)我們嘗試讀取未初始化變量的值時(shí),Rust 編譯器會(huì)做什么。
fn main() {
let a: i32;
a = 123;
println!("a: {a}");
let b: i32;
println!("b: ");
b = 123;
}
這里,我有兩個(gè)不可變變量 a
和 b
,在聲明時(shí)都沒(méi)有初始化。變量 a
在其值被讀取之前被賦予了一個(gè)值。但是變量 b
的值在被賦予初始值之前被讀取了。
來(lái)編譯一下,看看結(jié)果。
$ 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` 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 編譯器拋出了一個(gè)編譯時(shí)錯(cuò)誤和一個(gè)警告。警告說(shuō)變量 b
的值從來(lái)沒(méi)有被讀取過(guò)。
但是這是荒謬的!變量 b
的值在第 7 行被訪問(wèn)了。但是仔細(xì)看;警告是關(guān)于第 8 行的。這很令人困惑;讓我們暫時(shí)跳過(guò)這個(gè)警告,繼續(xù)看錯(cuò)誤。
這個(gè)錯(cuò)誤信息說(shuō) used binding b is possibly-uninitialized
。和之前的例子一樣,Rust 編譯器指出錯(cuò)誤是由于嘗試在第 7 行讀取變量 b
的值而引起的。讀取變量 b
的值是錯(cuò)誤的原因是它的值沒(méi)有初始化。在 Rust 編程語(yǔ)言中,這是非法的。因此編譯時(shí)錯(cuò)誤出現(xiàn)。
??? 這個(gè)錯(cuò)誤可以很容易地通過(guò)交換第 7 和第 8 行的代碼來(lái)解決。試一下,看看錯(cuò)誤是否消失了。
示例程序:交換數(shù)字
現(xiàn)在你已經(jīng)熟悉了常見(jiàn)的變量相關(guān)問(wèn)題,讓我們來(lái)看一個(gè)交換兩個(gè)變量值的程序。
fn main() {
let mut a = 7186932;
let mut b = 1276561;
println!("a: {a}, b: ");
// 交換變量值
let temp = a;
a = b;
b = temp;
println!("a: {}, b: {}", a, b);
}
我在這里聲明了兩個(gè)變量 a
和 b
。這兩個(gè)變量都是可變的,因?yàn)槲蚁M诤竺娓淖兯鼈兊闹怠N屹x予了一些隨機(jī)值。最初,我打印了這些變量的值。
然后,在第 8 行,我創(chuàng)建了一個(gè)名為 temp
的不可變變量,并將存儲(chǔ)在 a
中的值賦給它。之所以這個(gè)變量是不可變的,是因?yàn)?nbsp;temp
的值不會(huì)改變。
要交換值,我將變量 b
的值賦給變量 a
,在下一行,我將 temp
的值(它包含 a
的值)賦給變量 b
?,F(xiàn)在值已經(jīng)交換了,我打印了變量 a
和 b
的值。
在編譯并執(zhí)行上面的代碼后,我得到了以下輸出:
a: 7186932, b: 1276561
a: 1276561, b: 7186932
正如你所見(jiàn),值已經(jīng)交換了。完美。
使用未使用的變量
當(dāng)你聲明了一些變量,打算在后面使用它們,但是還沒(méi)有使用它們,然后編譯你的 Rust 代碼來(lái)檢查一些東西時(shí),Rust 編譯器會(huì)警告你。
原因是顯而易見(jiàn)的。不會(huì)被使用的變量占用了不必要的初始化時(shí)間(CPU 周期)和內(nèi)存空間。如果不會(huì)被使用,為什么要在程序?qū)懮纤??盡管編譯器確實(shí)會(huì)優(yōu)化這一點(diǎn)。但是它仍然是一個(gè)問(wèn)題,因?yàn)樗鼤?huì)以多余的代碼的形式影響可讀性。
但是,有的時(shí)候,你可能會(huì)面對(duì)這樣的情況:創(chuàng)建一個(gè)變量與否不在你的控制之下。比如說(shuō),當(dāng)一個(gè)函數(shù)返回多個(gè)值,而你只需要其中的一些值時(shí)。在這種情況下,你不能要求庫(kù)維護(hù)者根據(jù)你的需要調(diào)整他們的函數(shù)。
所以,在這種情況下,你可以寫一個(gè)以下劃線開(kāi)頭的變量,Rust 編譯器將不再顯示這樣的警告。如果你真的不需要使用存儲(chǔ)在該未使用變量中的值,你可以簡(jiǎn)單地將其命名為 _
(下劃線),Rust 編譯器也會(huì)忽略它!
接下來(lái)的程序不僅不會(huì)生成任何輸出,而且也不會(huì)生成任何警告和/或錯(cuò)誤消息:
fn main() {
let _unnecessary_var = 0; // 沒(méi)有警告
let _ = 0.0; // 完全忽略
}
算術(shù)運(yùn)算
數(shù)學(xué)就是數(shù)學(xué),Rust 并沒(méi)有在這方面創(chuàng)新。你可以使用在其他編程語(yǔ)言(如 C、C++ 和/或 Java)中使用過(guò)的所有算術(shù)運(yùn)算符。
包含可以在 Rust 編程語(yǔ)言中使用的所有運(yùn)算符和它們的含義的完整列表可以在 這里 找到。
示例程序:一個(gè)生銹的溫度計(jì)
(LCTT 譯注:這里的溫度計(jì)“生銹”了是因?yàn)樗鞘褂?Rust(生銹)編寫的,原作者在這里玩了一個(gè)雙關(guān)。)
接下來(lái)是一個(gè)典型的程序,它將華氏度轉(zhuǎn)換為攝氏度,反之亦然。
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
);
}
沒(méi)什么大不了的……華氏溫度轉(zhuǎn)換為攝氏溫度,反之亦然。
正如你在這里看到的,由于 Rust 不允許自動(dòng)類型轉(zhuǎn)換,我不得不在整數(shù) 32、9 和 5 后放一個(gè)小數(shù)點(diǎn)。除此之外,這與你在 C、C++ 和/或 Java 中所做的類似。
作為練習(xí),嘗試編寫一個(gè)程序,找出給定數(shù)中有多少位數(shù)字。
常量
如果你有一些編程知識(shí),你可能知道這意味著什么。常量是一種特殊類型的變量,它的值永遠(yuǎn)不會(huì)改變。它保持不變。
在 Rust 編程語(yǔ)言中,使用以下語(yǔ)法聲明常量:
const CONSTANT_NAME: data_type = value;
如你所見(jiàn),聲明常量的語(yǔ)法與我們?cè)?Rust 中看到的變量聲明非常相似。但是有兩個(gè)不同之處:
- 常量的名字需要像
SCREAMING_SNAKE_CASE
這樣。所有的大寫字母和單詞之間用下劃線分隔。 - 常量的數(shù)據(jù)類型必須被顯性定義。
變量與常量的對(duì)比
你可能在想,既然變量默認(rèn)是不可變的,為什么語(yǔ)言還要包含常量呢?
接下來(lái)這個(gè)表格應(yīng)該可以幫助你消除疑慮。(如果你好奇并且想更好地理解這些區(qū)別,你可以看看我的博客,它詳細(xì)地展示了這些區(qū)別。)
一個(gè)展示 Rust 編程語(yǔ)言中變量和常量之間區(qū)別的表格
使用常量的示例程序:計(jì)算圓的面積
這是一個(gè)很直接的關(guān)于 Rust 中常量的簡(jiǎn)單程序。它計(jì)算圓的面積和周長(zhǎng)。
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!("有一個(gè)周長(zhǎng)為 {radius} 厘米的圓");
println!("它的面積是 {} 平方厘米", circle_area);
println!(
"以及它的周長(zhǎng)是 {} 厘米",
circle_perimeter
);
}
如果運(yùn)行代碼,將產(chǎn)生以下輸出:
有一個(gè)周長(zhǎng)為 50 厘米的圓
它的面積是 7850 平方厘米
以及它的周長(zhǎng)是 314 厘米
Rust 中的變量遮蔽
如果你是一個(gè) C++ 程序員,你可能已經(jīng)知道我在說(shuō)什么了。當(dāng)程序員聲明一個(gè)與已經(jīng)聲明的變量同名的新變量時(shí),這就是變量遮蔽。
與 C++ 不同,Rust 允許你在同一作用域中執(zhí)行變量遮蔽!
?? 當(dāng)程序員遮蔽一個(gè)已經(jīng)存在的變量時(shí),新變量會(huì)被分配一個(gè)新的內(nèi)存地址,但是使用與現(xiàn)有變量相同的名稱引用。
來(lái)看看它在 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);
let mut b = 120;
println!("b的地址: {:p}, b的值: // 遮蔽后", &b);
let mut c = 18;
println!("\nc 的地址: {:p}, c的值: {c}", &c);
c = 29;
println!("c 的地址: {:p}, c的值: {c} // 遮蔽后", &c);
}
println
語(yǔ)句中花括號(hào)內(nèi)的 :p
與 C 中的 %p
類似。它指定值的格式為內(nèi)存地址(指針)。
我在這里使用了 3 個(gè)變量。變量 a
是不可變的,并且在第 4 行被遮蔽。變量 b
是可變的,并且在第 9 行也被遮蔽。變量 c
是可變的,但是在第 14 行,只有它的值被改變了。它沒(méi)有被遮蔽。
現(xiàn)在,讓我們看看輸出。
a 的地址: 0x7ffe954bf614, a 的值 108
a 的地址: 0x7ffe954bf674, a 的值: 56 // 遮蔽后
b 的地址: 0x7ffe954bf6d4, b 的值: 82
b 的地址: 0x7ffe954bf734, b 的值: 120 // 遮蔽后
c 的地址: 0x7ffe954bf734, c 的值: 18
c 的地址: 0x7ffe954bf734, c 的值: 29 // 遮蔽后
來(lái)看看輸出,你會(huì)發(fā)現(xiàn)不僅所有三個(gè)變量的值都改變了,而且被遮蔽的變量的地址也不同(檢查十六進(jìn)制的最后幾個(gè)字符)。
變量 a
和 b
的內(nèi)存地址改變了。這意味著變量的可變性或不可變性并不是遮蔽變量的限制。
總結(jié)
本文介紹了 Rust 編程語(yǔ)言中的變量和常量。還介紹了算術(shù)運(yùn)算。
做個(gè)總結(jié):
- Rust 中的變量默認(rèn)是不可變的,但是可以引入可變性。
- 程序員需要顯式地指定變量的可變性。
- 常量總是不可變的,無(wú)論如何都需要類型注釋。
- 變量遮蔽是指使用與現(xiàn)有變量相同的名稱聲明一個(gè) 新 變量。
很好!我相信和 Rust 一起的進(jìn)展不錯(cuò)。在下一章中,我將討論 Rust 中的數(shù)據(jù)類型。敬請(qǐng)關(guān)注。
與此同時(shí),如果你有任何問(wèn)題,請(qǐng)告訴我。
(題圖:MJ/7c5366b8-f926-487e-9153-0a877145ca5)