此篇筆記主要以資訊工程研究所(在已經撰寫了幾個月的 C 語言之後)學習 Rust 的日誌

此外,所有學習紀錄都會放在我的 github 當中以供想要學習 rust-lang 的人觀看。並且科技詞彙增進自身的漢語表達能力。

Lesson-1 基礎語法以及工具

和所有程式語言一樣,Rust 也有一個主程式,通常叫做 main。而就像在學習所有新語言的朋友們,我們還是必須要形式上的跟世界說嗨!

首先可以定義一個新的檔案叫做 hello.rs,並且使用簡單的 command (注意!這邊假設已經安裝並且可以使用終端機來編譯 rust 語言,若不知道的人可以看這裡

fn main() { println!("Hello World"); }
  • 第一個映入眼簾的就是 main 是使用 fn 來定義而不是和 C 語言一樣必須要求回傳的檔案格式。
  • 另外一個值得注意的是 println! 最後面的驚嘆號,Rust 官方解釋是這是一個巨集(macro)的展開。(將在更後續詳細探討)

定義好了之後就可以使用 rustc (c for compile) 來編譯 rustfile

rustc .\hello.rs

Rust 有一些規範,例如不是用 [TAB] 來縮行,而是使用四個空白

另外,rust 語言把編譯以及執行分開,這樣一來不需要在電腦裡面安裝 rust 也可以執行相對的檔案。

  • 眼尖的可能會在當前目錄下看到一個 .pdb 的檔案,rust 官方文件說這個檔案存在除錯資訊(目前沒有細說)

Cargo tools

當專案越來越大時,勢必需要有管理專案的工具。而 cargo 這個工具(在安裝時 rust 時即安裝好)可以幫忙下載函式庫以及協助專案管理。

使用以下指令即可利用 cargo 做出一個 rust 的專案

$ cargo new new_proj

>> Created binary (application) `new_proj` package

使用 tree 指令可以看到以下的空專案內容,可以看到專案使用 TOML 來管理,以及在 src 目錄下可以看到一個 main.rs(預設為先前的檔案)

.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files
# 注意,cargo 會預設幫你建立 .git 檔,若已經存在有 .git 的專案底下則不會生成。

.toml

  • 這個檔案其實就是這個專案的所有資訊(包括 dependency 以及版本等等)

先來打開檔案來瞧瞧

[package]
name = "new_proj"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

首先看到 [package],也就是當前的專案, [dependencies] 也就是用到的套件都會登記在這邊

接續 cargo

這邊要注意 cargo 專案中的程式碼都必須放在 src 目錄下

而在專案當中也只需要簡單的一行,cargo 即可幫忙把所有檔案連結起來,以下為範例
(cargo 在沒有更改原始碼的狀態下不會重新編譯,基本上就是全部都已經撰寫好的 Makefile)

$ cargo build    # 編譯並且 link 
$ cargo run      # 編譯 + link + 跑程式
$ cargo check    # 可以常常使用這個來確定你寫的 rust 程式碼可不可以過編譯,因為不是編譯,所以快很多。
  • 紀錄,在 cargo build/run 之後找不到可執行檔,首先要看 toml 裡面的 name,接下來,通常可執行檔的位置會存放在 <project>/target/debug/ 這個目錄底下。

Lesson-2 實作猜數字遊戲以及補充語法說明

根據此教程實作出一個猜數字遊戲。我覺得直接看程式碼來理解 rust 會比較快

/* src/main.rs */ use std::io; fn main() { println!("Guess the number!"); println!("Please input your guess."); let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("Failed to read line"); println!("You guessed: {guess}"); }

上面這個程式碼最重要的地方無外乎為第 9 以及 11 至 13 行。以下來稍微解釋這幾行到底做了些甚麼

let mut guess = String::new();

Rust 中存在著可變動以及不可變動的變數,let 宣告一個變數,而 mut 讓這個變數可以在後續被修改,而 = 把後面的資訊綁定給這個關鍵字。廢話不多說,馬上來實驗看看,以下為例子:

let a = 1; a = 2;

可以看到第一行 let 定義了一個變數為 a,而這個變數在第二行的時候我嘗試賦值給他,接下來使用 cargo check 就會跑出以下錯誤訊息:

15 |     let a = 1;
   |         -
   |         |
   |         first assignment to `a`
   |         help: consider making this binding mutable: `mut a`
16 |     a = 2;
   |     ^^^^^ cannot assign twice to immutable variable

可以看到 rust-compiler 提示沒有 mut 的資料無法附值兩次。只需要改成 let mut a = 1 就可以更改值了。

再來注意到剛剛第 12 行的部分,&mut guess,跟 C 語言一樣,加入 & 字符就代表 pass by reference,也就是把記憶體位置送進 function 中,但和其他語言不一樣的是:送進去也需要考慮這個東西是不是可變的 mut,也就是說,如果把 &guess 替換成原本的引數(在這個情況下會出問題)

這個會在 Lesson-4 中提到

再來看到最後的 .except(),手冊上提到這個是用來除錯使用的程序,當做完 read_line 之後,內部會有一個 Result,這個 Result 主要會有兩大項目,一個是 OK,另外一個是 Err(作用有點類似 Errno)

最後就是 std output,這邊不多敘述,因為 {} 的作用與 C 語言中針對型別的 %d, %ld ... 差不多。

額外補充一點,cargo 這個工具還可以直接把當前 dependency 中的所有文件利用網頁顯示給你看(包括範例)只需要在當前目錄下使用 cargo doc --open,如此一來所有資訊都會在你面前顯示,這樣也不用擔心不知道要怎麼用。

接下來就是比較的時間了,以下程式碼為官方文件的程式碼。

    // after insert guessing number, time to compare
    match guess.cmp(&secret_number) {
        Ordering::Greater => println!("too big"),
        Ordering::Less => println!("too small"),
        Ordering::Equal => println!("you win")
    }

但以上的程式碼沒有辦法過編譯,由於先前輸入的 guess 是一個字串,而這邊放入的 secret_number 為整數,故無法做比較。值得在意的是這邊的 Ordering 以及 match,他是一個 enum(Lesson-6/18 會細講),這個 enum 讓我們確保可以 handle 各種狀況。

要如何解決並且轉型別?

在不同程式語言中,解析文字並且得到自己想要的資料是一個非常重要的事情。如同 python 給予的簡易 __call__ 讓我們可以在各種型別做不同的轉換,例如

def string_mutiply(str1: str, str2: str):
    return str(int(str1) * int(str2))

如此在 C 語言非常困難的型別轉換,在這邊可以直接利用以下程式碼的 : <type> = ***.parse() 來轉換

let guess: u32 = guess.trim().parse().expect("Please type a number");

以上示範了怎麼樣用字串做轉型,等等 Lesson3 會細說,這邊只需知道拿來做轉換即可。

最後,我們當然須要讓使用者不停輸入,這樣才可以讓使用者不停的猜,直到猜對。那勢必要把迴圈拿出來用了,以下為 loop 的範例

loop {
    // code here
}

作用與 while(1) 是相同的。再來只需要當猜中數字的時候跳脫迴圈即可

loop{ println!("You guessed {guess}"); let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(errno) => { println!("errno : {errno}"); continue;}, }; match guess.cmp(&secret_number) { Ordering::Greater => println!("too big"), Ordering::Less => println!("too small"), Ordering::Equal => { println!("you win"); break; } } }

如此一來,當猜中數字之後就可以跳脫迴圈。

這邊可以注意到第 3 行的部分,把後面的 except (也就是錯誤時跳出錯誤並且停掉主程式) 而改成 match 讓後續回傳剛剛所說的 enum,如此一來我們就可以處理錯誤,而不是跳脫錯誤了。

至此,我們已經完成了 rust 的第一個猜數字專案。

Lesson-3 variable types, function, control flow

這個章節會特別注重於型別以及相關規範

Const v.s. immutables

在前一個章節我一直有一個疑問,為甚麼宣告變數不用型別,為甚麼要分可變動 (mutable) 跟不可變動 (immutable),而且為甚麼存在不可變動變數時,又存在常數 (const):原來是為了使編譯更快速,以及讓程式開發者更好的去調整

常數 constants

在 Rust 的命名邏輯中,常數通常以 全大寫中間以底線分開,這兩個規則命名,當定義了常數例如:

const THREE_HOURS_IN_SECOND: u32 = 60 * 60 * 3;

編譯器會將這個結果 10800 給定義為常數,而執行時也不會真正執行三次乘法(浪費時間)

將常數定義在好的位置有助於解釋程式碼以及加速開發

變數 variables

變數又分為可變動以及不可變動(先前已經提過),而跟 const 最大的不同在於 shadowing,有點類似重新宣告。
我們可以用以下程式碼來測試看看 var 跟 const 的差別

const THREE_HOURS_IN_SECOND: u32 = 60 * 60 * 3; fn main() { println!("{THREE_HOURS_IN_SECOND}"); let THREE_HOURS_IN_SECOND = THREE_HOURS_IN_SECOND * 3; println!("{THREE_HOURS_IN_SECOND}"); }

這樣沒有辦法編譯,原因就是因為這個 THREE_HOURS_IN_SECOND 並不是變數,他沒有辦法再這個 scope 下被 shadowing

讓我們用另外一個形式試試看

fn main() { let THREE_HOURS_IN_SECOND = 60 * 60 * 3; println!("{THREE_HOURS_IN_SECOND}"); let THREE_HOURS_IN_SECOND = THREE_HOURS_IN_SECOND * 3; println!("{THREE_HOURS_IN_SECOND}"); }

這樣就可以編譯成功了,但還是不建議將變數命名成常數的樣子

整體來說 let 讓我們可以重新定義這個變數而不會

implicit and explicit

Rust 比 C 好的一點即利用 let 可以宣告所有類型的變數(看你怎麼樣賦值),而也可以和 compiler 宣告清楚這個型別:

let a = 1; // compiler 認定為有號 32 位元整數
let b = 3.12; // 有號 32 位元浮點數
let c: i32 = 1;
let d: f32 = 3.12;

上述的四行會做到一樣的事情,就看程式設計者想要怎麼設計了。

有很多寫法與 C 語言雷同,這邊不會贅述,以下只會給予不同型別宣告的範例

/* scalar type */ let a = 1; let a: i8 i16 i32 i64 i128 = 1; /* 前面取一個用 */ let a: u8 u16 u32 u64 u128 = 1; /* 同理 */ let a: bool = false; let a: char = '😻'; /* compound type */ // 1. tuple let tup: (i32, u32, bool) = (1, 3, true); // 取出的方法如下: println!("{tup.0}, {tup.1}, {tup.2}"); // 2. array 有幾種不同的宣告方法,後面會做說明 let a = [1, 2, 3, 4, 5]; // 直接賦值 let a = ['1', '2', '3']; // 以此類推,可以在 array 內部宣告相同的 dtype let a: [i32; 5]; // 代表這個 arr 中有 5 個 i32 的值 let a: [3; 5]; // 代表這個 arr 中有 5 個 3 的數值 = [3, 3, 3, 3, 3] // 取出的方法如下: println!("{a[0]}, {a[1]}, {a[2]}");

函式 function

至此,我覺得最困難的就是 function,因為有非常多種宣告方法。這邊是語法最容易搞混的地方了。所以接下來我都會盡可能的用解釋帶例子讓讀者熟悉寫法。

  • normal function declaration
    • 一般來說,只需要在最前面放上 fn 的關鍵字就可以宣告一個 function
fn function () {}

到此為止應該會有人想問,為甚麼沒有回傳任何值,那是因為在 rust 當中並沒有 void function,在每一個 function 都必須要有一個箭頭定義他的回傳型別

比較特別的就是在一個花括號的 scope 當中,預設最後一個 expression 為回傳值
所以可以製作出下方的特殊函式

fn zero () -> i32 { return 0; } fn zero_no_return () -> i32 { 0 }

這兩個函式做了完全一樣的事情,回傳 0,但第五行為這個花括號的 scope 當中最後一個 expression,所以預設為回傳值

也可以用這個方法來定義變數

let y: i32 = {
    let x: i32 = 15;
    15 + 5
}

同理,花括號當中最後一個 expression 為回傳值,所以 y 理當為 20

Control Flow

終於來到怎麼樣撰寫 if else 了,接下來會介紹語法以及特殊寫法。

if expr {
    // do something    
} else {
    // do other thing
}

和 C 語言最大的不同點為,expr 必須要布林值 (bool),而不是大於等於 1 就為 true

You must be explicit and always provide if with a Boolean as its condition.

到目前為止,我們學到了一個 scope 是可以有回傳值的,我們馬上來應用一下(這是到目前為止我覺得最刺激最好玩的部分)

let condition = true;
let variable = if condition {5} else {6};

這邊應該可以猜到會出現什麼樣的東西了 (可以利用這種定義方法做出寫出很好玩的 code)

     Running `target/debug/Lesson_3_types`
5

loop

loop 這個關鍵字定義了類似 while true 的行為,但又和後者不同,最特別的點在於可以回傳值,也就是在 break 後面接上一個要回傳的值,可以當作這個 scopereturn value

let result = loop {
    counter += 1;

    if counter == 10 {
        break counter * 2;
    }
};

再來另外一個特別的地方就是,break 也可以當作多層迴圈中用來控制流程的關鍵字,以下為例子:

fn main() { let mut count = 0; 'counting_up: loop { println!("count = {count}"); let mut remaining = 10; loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1; } println!("End count = {count}"); }

第一次看到的人一定會覺得很奇怪,為甚麼第 3 行長成這樣?這邊有點類似 C 語言中的 goto,在其他語言若要跳脫多重迴圈,比較簡易的方法是使用 goto, return 或是用多個 if else 去管理,後者會導致寫出來的程式碼缺少了美感並且不容易懂。從這邊即可得知這個 break 以及 label 的設計意義了。並且從剛剛的教學可以看到,break 後面可以接一個回傳值,那在這種跳脫多層迴圈的狀況呢?

非常簡單,只需要在 label 後面放入回傳值即可

-   break 'counting_up;
+   break 'counting_up 777;

若把前面的第 13 行改為以上的樣子,就可以在這個 scope 當中回傳 777(不過還是記得要把它改成變數型式)

while for

就與大部分程式語言一樣,當 while 後方的 condition 為真時,會不停的跑內部的行為

for 的語法反而與 python 雷同

let a = [10, 20, 30, 40, 50];

for element in a {
    println!("the value is: {element}");
}

輸出如下:

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

如此方便的語法,可以協助我們爾後在不確定 array 長度時遍歷整個 array

這個 section 的最後,我想要講一下 .. 的功能

for i in (1..100) {
    println!("{i}");
}

這邊的 .. 其實就是把頭尾之間的所有整數展開,幫我們更好定義了變數會走動的範圍。