前情提要
上次我們學習如何安裝 Rust,以及利用 Cargo 管理 Rust 專案,
今天則是要介紹 Rust 的變數概念、基本資料型別、函式以及控制流程,也就是常見的程式設計概念。
變數與可變性
相信大家對「變數(variable)」已經有基本概念,今天特別要說明的是在 Rust 中變數的特性。
變數
首先建立一個專案叫做 variables,並且編輯 src/main.rs:
1 | fn main() { |
然後 cargo run,應該會得到以下輸出:
1 | $ cargo run |
錯誤訊息告訴我們,「cannot assign twice to immutable variable」。
是的,在 Rust 中變數預設是不可變的。
Rust 預設當我們給予變數一個數值,這個數值是不會被改變的,以確保程式進行的穩定性。
但變數的可變性一向是很重要的,所以當我們需要讓一個變數的數值是可變的,
可以在宣告時加上 mut 關鍵字,例如下列程式碼:
1 | fn main() { |
再次執行,便能得到正確的輸出。
常數
常數也是一個常見的程式設計概念,在 Rust 中,常數與不可變變數的差異如下:
- 常數是「永遠不可變」,所以宣告時不可以使用
mut,並且若是使用const宣告,需要指明型別(在下一段會提及) - 常數可以被定義在任何有效範圍,包含全域
- 常數只能透過常數表達式設置
常數常用於將會擴散到所有程式碼的數值,在未來修改程式時,可以知道需要修改的部份。
遮蔽(Shadowing)
我們可以使用之前的變數再次告新的變數,而這個動作在 Rust 被稱為遮蔽,
代表編譯時編譯器會看到第二個變數的值,並佔據變數名稱的使用權直到該變數生命週期結束(在 Rust 中稱作「離開作用域」)。
下面是一個遮蔽的例子:
1 | fn main() { |
首先將 x 給予 5,然後重複使用 let x = 建立新變數 x,以 6(5 + 1)取代原本數值;
接著在 {} 內第三次出現的 let 陳述遮蔽了 x,所以輸出 x = 12;
離開 {} 後,內部的 let 造成的遮蔽結束,故 x 回到原本的 6。
遮蔽與 mut 不同之處,在於 mut 是將變數本身設為可變,而遮蔽是透過再次宣告改變內部儲存的資料;
另外,可以透過遮蔽,在產生新的變數同時更改型別,例如:
1 | fn main() { |
在第一次宣告時 space 是一個字串,但遮蔽後變成數字;
而 mut 是無法做到的。
簡而言之,遮蔽就是對一個變數的「重新宣告」。
基本資料型別
在 Rust 中,每個數值都有一個型別,可以告訴 Rust 資料指定的格式,以便 Rust 處理。
需要注意的是,Rust 是一個「靜態型別」語言,代表 Rust 在編譯時需要知道所有變數的型別,
儘管與現在多數程式語言一樣,Rust 能依據數值與使用方式推導變數的型別,
但遇上多種可能的型別時,還是需要明確指定。
純量型別
純量(Scalar),即是指單一數值,在 Rust 中包含整數、浮點數、布林與字元。
整數
不含小數點的數字,依據使用位元大小分為以下數種:
| 長度 | 帶號 | 非帶號 |
|---|---|---|
| 8 bits | i8 |
u8 |
| 16 bits | i16 |
u16 |
| 32 bits | i32 |
u32 |
| 64 bits | i64 |
u64 |
| 128 bits | i128 |
u128 |
| architecture | isize |
usize |
Rust 預設使用的整數型別是 i32,至於位元長度與可儲存的範圍,
(這個應該是在計概就說過的東西,此處不再提及,真的有需要以後再寫一篇www);isize 與 usize 則是依據系統架構決定(32 位元與 64 位元)。
在 Rust 中,可以透過「字面值(literals)」以指定型別,並且可以加上底線 _ 方便閱讀:
| 字面值 | 範例 |
|---|---|
| 十進制 | 98_222 |
| 十六進制 | 0xff |
| 八進制 | 0o77 |
| 二進制 | 0b1111_0000 |
位元組(限 u8) |
b’A’ |
注意:若是在變數名稱前加上 _,則是使變數成為不可被外部存取(類似 Java private)。
浮點數
有小數點的數字被稱為浮點數,在 Rust 中有兩種基本型別:f32 與 f64(依據 IEEE-754 定義浮點數之精度),
其中預設使用 f64,並且所有浮點數都是帶號數。
以上數值皆可以使用基本數值運算之運算子(+、-、*、/、%)。
布林
在 Rust 也有布林值 true 與 false,若要使用布林型別則是使用 bool,最常用於 if 表達式。
字元
Rust 有一個字元型別 char,使用方式如下述範例:
1 | fn main() { |
這邊需要提醒,char 使用單引號賦值(雙引號是字串),同時大小為四個位元組並且是 Unicode 純量數值(所以可以儲存亞洲文字甚至 emoji!)。
特別注意:一個「字元」並非真正的 Unicode 概念,關於這點往後會有討論。
複合型別
複合型別是組合數個數值為一個型別,在 Rust 有兩種基本複合型別:元組(tuple)與陣列(array)。
元組(Tuple)
元組有以下特點:
- 組合不同型別的數值
- 擁有固定長度,一旦宣告完成便無法增長或縮減
在 Rust 中,元組以 () 宣告,每個值以逗號分開;可以進行模式配對(pattern matching)以解構元組的數值,取得每個獨立數值。
例如:
1 | fn main() { |
若是需要取值,可以用 . 加上索引取得元素。例如:
1 | fn main() { |
另外,沒有任何數值的元組被稱為單元型別(unit),數值與型別都寫作 (),通常代表空數值或回傳型別;
一個表達式若無任何回傳數值會隱式回傳單元型別。
陣列
陣列就不用多說,我們都很熟悉(特別是資訊大一必學 C,陣列絕對是老朋友了),
不過 Rust 的陣列不同於其他語言,是固定長度的,這代表我們可以讓資料安全的分配在堆疊(stack)內;
而之後會提到一個特殊型別————向量(vector),與陣列相似但是允許變更長度。
當確認元素多寡不會改變時,使用陣列是非常好的選擇,例如月份:
1 | fn main() { |
在宣告陣列時可以寫出型別,如下列格式:
1 | fn main() { |
或是將所有元素數值設為相同:
1 | fn main() { |
至於獲取元素,跟陣列本身一樣,我們已經非常熟悉,就不再多提。
注意:Rust 會進行索引值檢查以保障記憶體安全,這是相較其他語言,比較特殊的功能。
函式
函式是非常普遍的程式概念,不論是程序式或者物件導向都很常見到,Rust 自然也不例外。
在 Rust 中,宣告函式需要使用關鍵字 fn,並且與變數相同,使用 snake case 命名。
下面是一個包含定義的範例:
1 | fn main() { |
注意,Rust 可以在作用域的任何地方定義函式。
參數
如同多數程式語言,Rust 函式可以擁有參數(parameters),並且定義在函式簽名(signatures)中,
使用函式時就可以傳遞確切數值作為引數(arguments);
我們可以改寫上面的範例:
1 | fn main() { |
注意,在 Rust 中,必須要在函式簽名宣告每個參數的型別,這是 Rust 刻意為之的設計,
避免編譯器需要額外花時間尋找資訊以確認使用的型別。
陳述式與表達式
函式是由一系列陳述式(statements)組成,最後可選擇加上表達式(expression);
而由於 Rust 是基於表達式(expression-based)的語言,所以需要特別注意兩者的差異:
- 陳述式是進行一些動作,並不回傳任何數值
- 表達式是計算並產生數值
可以看以下範例:
1 | fn main() { |
若是執行會看到錯誤訊息:
1 | $ cargo run |
因為 let y = 6 是一個陳述式,並不會回傳數值,所以 x 無法得到任何數值。
而表達式會計算出一個數值:
1 | fn main() { |
其中表達式
1 | { |
會回傳 4 給 let y = 以進行賦值。
注意 x + 1 並沒有加上分號 ;,
因為表達式結尾並不會加上分號,否則會導致表達式變成陳述式而不回傳數值;
另外在 Rust 中,數字本身也是一種表達式。
回傳值
在 Rust 中函式可以回傳數值給呼叫者,不過並不會為回傳值命名,而是在函式簽名上使用 -> 宣告回傳值的型別;
通常回傳值會是函式本體的最後一個表達式,但也可以使用 return 提早回傳。
回傳的範例如下:
1 | fn main() { |
控制流程
多數程式語言都有「依據某項條件是否為 true 執行特定程式碼」,以及「依據某項條件是否為 true 重複執行特定程式碼」的功能,
在 Rust 中,能夠讓我們控制流程的常見方法是 if 表達式以及迴圈。
if 表達式
if 是依據條件判斷產生分支(arms),若是為真則執行後續的程式碼;
另外也可以加上 else,在條件不符時執行另一段程式碼:
1 | fn main() { |
需要注意的是,判斷條件必須是 bool,否則會造成無法編譯。
else if:多重條件判斷
else if 能夠實現多重條件與分支。
Rust 的用法與其他語言沒有太大差異,就只提供範例:
1 | fn main() { |
不過,上述程式碼只會執行第一個條件為 true 的區塊;
若是要實現同時成立一個以上,可以透過 match 結構,這個會在往後提到。
在 let 陳述式中使用 if
因為 if 是一種表達式,所以可以用在 let 陳述式之中,並將結果賦值給變數:
1 | fn main() { |
上述程式碼會將 if 表達式運算的數值賦值給 number。
需要注意,可能成為結果的每個分支所回傳的值必須要是相同型別。
迴圈
迴圈跟 if 一樣是程式設計的老朋友,
在 Rust 中有三種迴圈:loop、while 與 for。
loop
loop 是不斷執行迴圈直到我們親自告訴迴圈停下來:
1 | fn main() { |
這段程式碼會一直印出 再一次!,也就是產生了無限迴圈,可以利用 Ctrl-C 或其他中斷方式強行切斷;
但是學過基本程式設計都知道,無限迴圈是一個很危險的東西,一不小心就會耗盡系統資源……blahblahblah。
不過跟其他語言類似,Rust 有提供中斷迴圈的關鍵字 break,以及跳過本次迴圈繼續執行的 continue。
迴圈與回傳
loop 可以在停止的時候回傳數值,這可以讓我們檢查執行緒是不是完成任務:
1 | fn main() { |
多重迴圈與迴圈標籤
有時候我們會在迴圈之中還有迴圈,這時候 break 與 continue 會用在最內層迴圈,
如果需要直接跳出外層迴圈, Rust 提供一個方式:迴圈標籤,可以直接針對迴圈進行操作:
1 | fn main() { |
其中 break 'counting_up 可以離開外層的迴圈。
while
while 則是「當條件為 true 時繼續執行迴圈」:
1 | fn main() { |
while 可以消除很多 loop、if、else 與 break 的結構,讓程式碼更容易閱讀。
for
for 多用於遍歷集合元素,儘管 while 也能做到,但使用 for 顯然更為簡潔。
以下是使用 while 的狀況:
1 | fn main() { |
而這是 for 的狀況:
1 | fn main() { |
可以看到 for 更為簡潔,並且可以避免程式錯誤(例如超出陣列大小)、增加安全性。
這邊要提到一個運算子:.. 範圍運算子。.. 前後可以放上起點與終點,例如 1..4 就是從 1 開始,遇到 4 停止(所以只會有 1 到 3)。
註解
啊好像還有一個東西沒說,但是這個不需要太多篇幅啦(而且寫到這邊我的原始檔已經 519 行了)。
在撰寫程式碼時,我們已經盡力讓程式碼更容易被閱讀,但有時免不了需要額外解釋,這就是註解(comments)的功能。
在 Rust 中,註解一律是使用 // 開頭,而 Rust 的註解格式,大多是位於說明目標的上一行:
1 | fn main() { |
另外 Rust 還有一種特殊的註解:技術文件註解,這在很後面提到發布 Crate 時才會用到。
總結
序章也才 260 行啊……怎麼這次直接翻倍了,害我說好平均三天一更馬上富奸……
這還只是基本概念而已欸……
這次把基本設計概念(大概就是大一上程式設計前半需要的概念)講完了,
平常可以多練習寫寫看,官方文件提供了幾個題目:
- 溫度轉換
- 產生費波那契數字
- 重複歌詞印出 The Twelve Days of Christmas
或是可以去找 LeetCode 題目練習(LeetCode 真的很好玩,雖然題目也很不好解就是了)。
下次要講到 Rust 最特殊的概念:所有權,會稍微複雜一點,希望在這之前我能自己讀的更懂(吐血
火山 / Kazan
2023.02.09