在這個章節將會介紹許多其它程式語言也都有的基礎概念,包含變數、資料型別、函數、註解以及條件和迴圈的流程控制。



變數

我們其實已經會用變數了,這邊再加強一下觀念。在上一章節中,我們提到了使用「let」宣告出來的變數是不可變的,如果要讓變數可變,需要加上「mut」關鍵字。這樣的設計習慣雖然和其它語言差異蠻大,但也是有它的優點在,如果變數預設被宣告出來後就可以一直被改變的話,我們很容易就會不小心改掉某個重要變數的值而導致程式執行結果有誤,通常發生這樣的情況後,會需要不少的時間才能找出問題所在,然而Rust程式語言這樣的設計習慣卻能在撰寫程式碼的階段就已經大大地減少這個情況發生的可能性了。

但所謂的「不可變的變數」,究竟是指「變數本身儲存的值」不會改變,還是「變數儲存的值本身」不會改變呢?

來做一個小實驗,程式如下:

注意程式第4行和第9行,我們分別對沒有使用「mut」的「s1」變數的值和「s1」變數本身所儲存的值進行修改的動作,程式編譯的結果如下:

rust-concepts

根據上圖,我們可以看到程式第4行和第9行都是無法通過編譯的!也就是說「不可變的變數」,「變數本身儲存的值」不會改變,且「變數儲存的值本身」也不會改變。若將「s1」變數加上「mut」關鍵字來宣告,程式即可通過編譯。

「不可變的變數」不就跟常數(constant)是一樣嗎?雖然它們看起來都是值不可以改變的名稱,但還是有一些差異。在Rust程式語言中,提供了「const」關鍵字來宣告常數,與用「let」宣告變數不同的是,「const」不允許加上「mut」關鍵字來讓常數變成可變的,因為「可變的常數」就根本不是常數了嘛!再來就是「const」宣告常數時,必須要明確地給出常數的型別,因為常數的值在Rust程式的編譯階段時,就需要確定下來了,換句話說,常數也無法儲存程式執行階段才能得到的資料。常數可以被宣告在任何的scope,包含全域的scope,且名稱習慣都使用大寫字母,並使用底線區隔不同的單字。例如:

常數存在的目的是要讓程式碼更容易維護,利用常數來表示程式碼之後可能會更動的設定值是很常見的用法。上一章提到的變數「遮蔽」,常數並沒有這種特性,如果在同一個程式scope下,宣告了相同名稱的常數,程式就會編譯錯誤。

有關於變數「遮蔽」,舉個一般的例子:

以上程式第4行和第6行,由於重複使用「x」這個名稱宣告變數,因此發生「遮蔽」。由於等號右邊會比左邊先執行,因此第4行等號右邊的「x」變數為第2行宣告的「x」變數,所以要進行「5 + 1」這個運算,並將結果「6」指派給第4行宣告出來的「x」變數儲存。同理,第6行等號右邊的「x」變數為第4行宣告的「x」變數,所以要進行「6 * 2」這個運算,並將結果「12」指派給第6行宣告出來的「x」變數儲存。所以最後程式會輸出:

The value of x is: 12

變數只需相同名稱即可「遮蔽」,因為其實就是再宣告出一個新的變數來用了,所以就算型別不同也可以。舉例來說:

以上程式第4行,由於重複使用「spaces」這個名稱宣告變數,因此發生「遮蔽」。第2行宣告出來的「spaces」變數型別為字串(確切來說是&str),第4行宣告出來的「spaces」變數型別為整數數值(確切來說是usize),兩者型別並不一樣,但程式是可以通過編譯的。

那麼「遮蔽」和「mut」關鍵字的差別在哪裡呢?除了「遮蔽」實際上會宣告出新的變數之外,它們不都是將相同名稱的變數所儲存的值進行改變嗎?以結果來說是這樣沒錯,但還是有用法上的差異。「mut」關鍵字並不能改變變數的型別,如以下程式:

這個程式將會編譯失敗,因為程式第4行,嘗試將整數型別的值指派給字串型別的「spaces」變數。

資料型別

Rust是「靜態型別」(Static Typing)的程式語言,在編譯階段就要完全決定好變數的型別,因此可以讓Rust程式在編譯階段就知道要怎麼樣儲存和處理該類型的資料。使用「let」關鍵字宣告變數的時候,通常可以不用事先指定好該變數的型別,因為編譯器會自動根據第一次指派給變數的值來推論變數的型別。舉例來說:

以上程式,變數「a」在宣告時並未指定型別,而且也沒有指派任何值。然而編譯器知道程式第4行會將「i32」型別的「2」指派給變數「a」,因此自動推論變數a的型別是「2」。

在某些情況下,由於「泛型」的關係有多種型別的可能,因此需要事先定義好變數的型別。前面的章節製作的猜數字程式,第二次宣告出來的「guest」變數就是一個需要事先明確定義好型別的例子。

純量型別(Scalar Types)

只有一個值的型別稱為「純量型別」,Rust程式語言中一共有四種基本的純量型別,分別是整數(integer)、浮點數(floating-point)、布林(boolean)、字元(character),底下將分別介紹這四種型別。

整數

沒有小數位數的數值就是整數,Rust程式語言對於整數數值型別,區分了有號整數(i)和無號整數(u),整數依照表示範圍區分為8位元、16位元、32位元、64位元和128位元。先前我們已經使用過了「i32」型別,用來表示32位元的有號整數,32位元的有號整數可以表示-231 ~ 231-1的整數數值範圍。如果是要使用8位元的有號整數,型別名稱為「i8」,依此類推。不同長度的有號整數的數值表示範圍公式如下:

in: -2n-1 ~ 2n-1-1

將「i32」的「i」改為「u」,變成「u32」,可用來表示32位元的無號整數,32位元的無號整數可以表示0 ~ 232-1的整數數值範圍。如果是要使用8位元的無號整數,型別名稱為「u8」,依此類推。不同長度的無號整數的數值表示範圍公式如下:

un: 0 ~ 2n-1

此外,有號整數和無號整數分別還有「isize」和「usize」這兩種型別能夠使用,它們的長度會跟程式的執行環境有關。如果程式是在32位元的環境下執行(如x86),則「isize」和「usize」都會使用32位元;如果程式是在32位元的環境下執行(如x86_64),則「isize」和「usize」都會使用64位元。

直接在Rust程式碼中撰寫整數數值的話,會直接當作「i32」型別來處理,如果預設使用「isize」的話會造成很多不確定的因素,如果預設使用「i64」則是會太佔儲存和運算資源。Rust程式語言支援的整數寫法有很多種,除了一般直接寫數字的方式外,還允許加上底線「_」來分割太多位數的整數。例如:

如同其它大部份程式語言,Rust的整數也支援16進制、8進制、2進制的表示方式。如下:

此外,還有一種針對字元值轉換成「u8」型別的寫法。如下:

執行結果為:

0 = 48, A = 65

有號整數的型別有「i8」、「i16」、「i32」、「i64」、「i128」和「isize」。無號整數的型別有「u8」、「u16」、「u32」、「u64」、「u128」和「usize」。請參考以下程式:

程式第8行,直接在程式碼寫的整數數值會預設使用「i32」型別。程式第9行,「2147483648」為231,已經超過「i32」的數值表示範圍,但Rust並不會自動使用「i64」來儲存,如果這行沒有被註解掉的話,編譯器會知道寫在程式碼的整數數值超出「i32」型別的表示範圍(溢位),編譯時就會出現警告訊息。但是程式第10行,直接在在程式碼中的整數數值後面接上「i64」,這個數值就會被轉型為「i64」,解決了溢位問題。程式第12行,使用「as」關鍵字也可以解決溢位問題;程式第13行,「9223372036854775808i128」為263,已經超過「i32」和「i64」的數值表示範圍,但Rust並不會自動使用「i128」來儲存,如果這行沒有被註解掉的話,編譯器會知道寫在程式碼的整數數值超出「i32」型別的表示範圍(溢位),編譯時就會出現警告訊息。但是程式第14行,直接在在程式碼中的整數數值後面接上「i128」,這個數值就會被轉型為「i128」,解決了溢位問題。程式第16行,使用「as」關鍵字也可以解決溢位問題。

再整理一下,自行定義寫在程式碼的整數數值型別的方式有三種:第一種是在整數數值之後直接接上整數型別名稱(程式第2、5、10、14、18、22、25、28、31行),第二種是透過宣告變數時定義變數的型別(程式第3、6、11、15、19、23、26、29、32行),第三種是使用「as」關鍵字來轉型(程式第4、7、12、16、20、24、27、30、33行)。這裡要注意一下,浮點數數值無法直接接上整數型別名稱來轉成整數型別。

浮點數

有小數位數的數值就是浮點數,Rust程式語言對於浮點數數值型別,依照精準度區,將長度分為32位元的「f32」(單精準浮點數)和64位元的「f64」(雙精準浮點數)。直接在Rust程式碼中撰寫浮點數數值的話,會直接當作「f64」型別來處理,因為「f64」能夠提供比「f32」還要更精確的數值,且運算速度在現代的CPU上是差不多的。

浮點數的型別有「f32」、「f64」。請參考以下程式:

程式第9行,直接在程式碼寫的浮點數數值會預設使用「f64」型別。

自行定義寫在程式碼的浮點數數值型別的方式有三種:第一種是在整數數值或浮點數數值之後直接接上浮點數型別名稱(程式第2、3、8行),第二種是透過宣告變數時定義變數的型別(程式第4行),第三種是使用「as」關鍵字來轉型(程式第5、6、10行)。

浮點數和整數型別之間的轉換一定要透過「as」關鍵字。以下的寫法都會編譯失敗:

必須改用「as」關鍵字才可以編譯成功:

如果是整數(或浮點數)變數要轉換成其它整數(或浮點數)型別,方式和直接寫在程式碼的數值一樣:透過宣告變數時定義變數的型別,或是使用「as」關鍵字來轉型,而浮點數和整數型別之間的轉換也是一定要透過「as」關鍵字。

布林

和其它大多數的程式語言一樣,Rust的布林型別也只有「true」和「false」兩種值。布林的型別名稱為「bool」。

字元

除了數值類型的型別之外,Rust程式語言也支援「char」型別,可以用來處理字元。直接寫在程式碼的字元必須使用單引號「'」包起來,例如:

「char」型別可以表示「Unicode Scalar」編碼的值,也就是說,除了ASCII字元外,還可以表示中文、日文、韓文(CJK)的文字,也能表示表情符號。

複合型別(Compound Types)

複合型別可以將不同型別的值組合成一個型別。Rust程式語言提供了兩種基本的複合型別,分別是「數組(tuple)」和「陣列(array)」。

數組

數組是一個將不同型別的值組合成一種型別的常用方式。在Rust程式語言中,可以利用逗號「,」在小括號「()」內分隔不同型別的值,就能將它們組合成「數組」。例如:

以上程式,t變數的型別就是一種數組型別,可以用「(i32, f64, i32, char)」來表示。換句話說,如果要在宣告變數時明確定義變數的數組型別,可以將程式寫成:

由於數組並未定義每個值所屬欄位的名稱,但因其有順序性,因此可以利用以下程式的語法,快速地將數組中的值指派給不同變數。

這裡要注意的是,只有在使用「let」關鍵字宣告變數時,才可以直接將數組拆開。如果程式是寫成以下這樣的話,會編譯失敗:

以上程式需要改寫成以下的樣子,才能通過編譯:

數組的順序是從索引0開始計算,可以利用「.」符號使用索引值指定要存取的數組欄位。數組的索引值在程式編譯階段就要決定好,無法使用變數或是常數代替。

以上程式,將數組t索引2的「1」存到索引0的位置,取代掉原本索引0的「500」。

如果嘗試使用超出數組範圍的索引值,在程式會編譯失敗。例如:

編譯結果如下圖:

rust-concepts

陣列

陣列的概念像是數組的簡化版,數組可以儲存多個不同型態的值,而陣列只能儲存多個相同型態的值(在陣列中稱為元素值(element))。Rust程式語言的陣列為固定長度,既不能增長也不能縮小,且長度在編譯階段時就要決定。利用逗號「,」在中括號「[]」內分隔相同型別的值,就能將它們組合成「陣列」。例如:

以上程式,a變數的型別就是一種陣列型別,且長度為5,可以用「[i32; 5]」來表示。換句話說,如果要在宣告變數時明確定義變數的陣列型別和陣列長度,可以將程式寫成:

與數組相同,由於陣列並未定義每個元素所屬欄位的名稱,但因其有順序性,因此可以利用以下程式的語法,快速地將陣列中的元素指派給不同變數。(這個用法屬於「slice_patterns」特色,由於其還在實驗階段,所以需加上「#![feature(slice_patterns)]」,並使用Rust的nightly版本來編譯程式)

同樣地,只有在使用「let」關鍵字宣告變數時,才可以直接將陣列拆開。如果程式是寫成以下這樣的話,會編譯失敗:

以上程式需要改寫成以下的樣子,才能通過編譯:

陣列的順序是從索引0開始計算,可以利用中括號「[]」使用索引值指定要存取的陣列欄位。陣列的索引值可以在程式執行階段才決定。

以上程式,將陣列a索引2的「3」存到索引0的位置,取代掉原本索引0的「1」。

此外,也可以利用陣列所提供的「get」方法來取得元素值。

使用「get」方法回傳的陣列元素值會被「Option」列舉包裹起來。「Option」列舉在之後的章節會作介紹。

利用中括號「[]」使用超出陣列長度範圍的索引值嘗試存取元素值,程式雖然可以編譯,但執行時會發生panic。例如:

執行結果如下圖:

rust-concepts

就算陣列的索引值在編譯階段就已決定好,編譯器也只會出現警告訊息,而不是直接出現編譯錯誤。「當索引值超出陣列範圍時,程式在執行階段發生panic」,這就表示Rust程式語言在程式執行階段會去維護陣列的長度範圍,雖然會消耗一些運算資源,但這樣的作法會比如C、C++這樣的底層語言,當索引值超出陣列範圍時還能繼續操作記憶體,來得安全許多。

如果要建立出一個固定長度的陣列,但陣列元素目前還無法確定的話,也可以直接將陣列寫成:

舉例來說:

函數

程式語言最重要的觀念就是函數啦!我們先前已經有使用過「fn」關鍵字來定義「main」函數,並且也知道「main」函數為特別的函數,被用來作為程式一開始執行的函數。若要建立出新的函數,同樣使用「fn」關鍵字,格式如下:

例如:

Rust程式語言並未限制定義函數的順序。舉例來說,以下兩個程式都是可以編譯的:

函數的名稱和參數的型別可以組合成函數的「簽名」,「簽名」相同的函數視為相同的函數。以上面的程式來說,在「main」方法中呼叫了「another_function("world")」,由於寫在程式碼中的字串的型別為「&str」,因此編譯器知道呼叫的函數簽名為「another_function(&str)」,所以會執行到我們定義的「another_function(name: &str)」函數,最後印出「Hi, world.」。

定義函數時使用的大括號「{}」包裹住的區塊為函數的主體,該區塊可由一行或多行敘述組成,且最後一行的敘述如果不加分號,表示會回傳該敘述的值。事實上,這整個主體區塊也可以看作是程式敘述的表達式(expression),且不限於使用在「fn」關鍵字。舉例來說:

以上程式第2行到第6行,我們也使用了大括號「{}」定義了一個新的程式區塊。在這個區塊中,會先印出「y = 10, z = 4」,然後計算並回傳變數「y」和變數「z」的結果。而這個回傳的結果會指派給「main」函數的「x」變數,最後再印出「x = 14」。

如果要定義一個有回傳值的函數,同樣是使用「fn」關鍵字,只不過需要多加上回傳值的型態。如下:

我們在函數的簽名和主體之間,多加了「->」符號來定義函數回傳值的型態。利用這樣的語法,來將上面的「x=y+z」程式改寫看看:

以上程式第2行,將「x」函數的回傳值指派給宣告出來的變數「x」。雖然變數和函數的用法不同,但如果都定義成相同名稱的話,也是會無法一起使用。第4行如果解除註解,程式就會編譯失敗。而第2行因等號右邊的程式會先執行的關係,所以呼叫「x」函數時還沒有將「x」變數宣告出來,因此可以編譯成功。

註解

Rust程式語言的註解都是由「//」開頭,直到該行結束。例如:

在程式碼中加入適當的註解可以讓程式變得更容易理解。

流程控制

大部分的程式語言可以藉由判斷條件來決定要執行哪些程式,以及重複執行哪些程式。

if條件表達式

Rust程式語言提供了「if」關鍵字和「else」關鍵字,可以很方便地實作出帶有條件判斷功能的表達式。用法如下:

「else」關鍵字以及其之後的程式敘述區塊可以省略不寫。

舉例來說:

程式執行結果:

condition was true

若將「number」變數的值改為「7」。

程式執行結果:

condition was false

如果需要進行多個條件的判斷,可以使用「else if」關鍵字。用法如下:

「else」關鍵字以及其之後的程式敘述區塊可以省略不寫。

舉例來說:

程式執行結果:

number is divisible by 3

由於「if」關鍵字實作出來的程式碼結構,可整體看作是一個表達式,可以利用大括號「{}」程式敘述區塊內,最後一行程式敘述不加分號的語法直接回傳數值。

舉例來說:

程式執行結果:

The value of number is: 5

因為在不管什麼條件下都一定要回傳數值,因此這樣的「if」結構用法必須要有「else」關鍵字,並且回傳值的型態都要一致,否則會編譯失敗。

迴圈

迴圈可以讓相同的程式自動執行一次以上,Rust程式語言提供了「loop」、「while」、「for」關鍵字來建立用途不同的迴圈。

loop迴圈

「loop」關鍵字所建立的迴圈,必須在迴圈的程式區塊內明確使用「break」敘述來脫離迴圈,否則會一直重複執行下去。用法如下:

舉例來說:

以上程式執行之後,程式將會不停地將「again!」一行一行地印在螢幕上。在CLI(命令列介面)模式下,可以使用快速鍵「Ctrl + c」來中斷程式的執行。

如果將程式改寫成:

我們在程式第4行加入了「break」敘述,因此程式在執行之後,只會印出一行「again!」,然後就跳出迴圈,結束執行了。

如果只是要跳過這次迴圈的執行,直接到下一次的話,可以使用「continue」敘述。舉例來說:

執行結果為:

2
check if number == 10
4
check if number == 10
6
check if number == 10
8
check if number == 10
10
check if number == 10

while迴圈

「while」關鍵字所建立的迴圈可以藉由判斷布林值來決定要不要脫離迴圈。用法如下:

舉例來說:

以上程式,當「number」變數不等於0的時候就會一直執行while迴圈內的程式敘述。執行結果如下:

3!
2!
1!
LIFTOFF!!!

while迴圈內的程式敘述區塊也可以使用「break」和「continue」敘述。

for迴圈

先繼續剛才介紹的while迴圈,我們可以利用while迴圈來走訪一個陣列。例如:

程式執行結果:

the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

雖然程式執行結果是正確的,但這樣的實作方式經常會不小心因為陣列長度沒有設好或算好,超出範圍而導致程式發生panic。而且就算是使用正確長度,這樣的寫法執行效率也不是很好,原因我們在先前有提到,那就是Rust會去檢查陣列的索引值有沒有超出範圍再去存取。為了增加安全性和增進效能,可以使用「for」關鍵字實作的迴圈來走訪陣列,用法如下:

「iter」方法是陣列型別內建的方法,會回傳一個「Iter」結構實體,作為迭代器(Iterator)。

改寫剛才的while迴圈:

另外也可以將陣列參考直接作為迭代器使用。如下:

除了使用陣列本身提供的迭代器之外,for迴圈也可以搭配範圍(range)語法所產生的某數值範圍的迭代器來使用。用法如下:

舉例來說:

程式執行結果:

1!
2!
3!
LIFTOFF!!!

這裡要注意到範圍語法的數值範圍最大值必須要加一,才會真正走訪到數值範圍的最大值。「1..4」語法表示的範圍為「1~3」;「2..6」語法表示的範圍為「2~5」。如果要剛好讓範圍語法能夠走訪到數值範圍最大值,而不必特別加一的話,可以加上等於「=」字元。用法如下:

「1..=4」語法表示的範圍為「1~4」;「2..=6」語法表示的範圍為「2~6」。

另外,迭代器也可以對相同資料提供許多不同的走訪方法,例如「rev」方法可以顛倒走訪順序。如下:

程式執行結果:

3!
2!
1!
LIFTOFF!!!

總結

在這個章節中,我們學會了Rust程式語言的變數、資料型別、函數、註解以及條件和迴圈的流程控制。在下一章節中,我們將會學習Rust程式語言跟其它大部份程式語言不一樣概念:擁有權(ownership)。