前情提要
上次我們學習如何安裝 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