此篇筆記主要以資訊工程研究所(在已經撰寫了幾個月的 C 語言之後)學習 [Rust](https://www.rust-lang.org/zh-TW) 的日誌 此外,所有學習紀錄都會放在我的 github 當中以供想要學習 rust-lang 的人觀看。並且[科技詞彙](https://hackmd.io/@sysprog/it-vocabulary)增進自身的漢語表達能力。 * [joshua0321 github 連結](https://github.com/JoshuaLee0321/rust-lang) # Lesson-1 基礎語法以及工具 和所有程式語言一樣,Rust 也有一個主程式,通常叫做 [main](https://www.quora.com/Why-is-the-main-function-called-the-main-function-and-not-something-else)。而就像在學習所有新語言的朋友們,我們還是必須要形式上的跟世界說嗨! 首先可以定義一個新的檔案叫做 `hello.rs`,並且使用簡單的 command (注意!這邊假設已經安裝並且可以使用終端機來編譯 rust 語言,若不知道的人可以看[這裡](https://doc.rust-lang.org/book/ch01-01-installation.html)) ```rust= fn main() { println!("Hello World"); } ``` * 第一個映入眼簾的就是 main 是使用 `fn` 來定義而不是和 C 語言一樣必須要求回傳的檔案格式。 * 另外一個值得注意的是 `println!` 最後面的驚嘆號,Rust 官方解釋是這是一個巨集(macro)的展開。(將在更後續詳細探討) 定義好了之後就可以使用 rustc (c for compile) 來編譯 rustfile ```bash rustc .\hello.rs ``` :::info Rust 有一些規範,例如不是用 `[TAB]` 來縮行,而是使用**四個空白** 另外,rust 語言把編譯以及執行分開,這樣一來不需要在電腦裡面安裝 rust 也可以執行相對的檔案。 * 眼尖的可能會在當前目錄下看到一個 `.pdb` 的檔案,rust 官方文件說這個檔案存在除錯資訊(目前沒有細說) ::: ## Cargo tools 當專案越來越大時,勢必需要有管理專案的工具。而 cargo 這個工具(在安裝時 rust 時即安裝好)可以幫忙下載函式庫以及協助專案管理。 使用以下指令即可利用 cargo 做出一個 rust 的專案 ```bash $ cargo new new_proj >> Created binary (application) `new_proj` package ``` 使用 `tree` 指令可以看到以下的空專案內容,可以看到專案使用 [TOML](https://toml.io/en/) 來管理,以及在 src 目錄下可以看到一個 `main.rs`(預設為先前的檔案) ```bash . ├── Cargo.toml └── src └── main.rs 1 directory, 2 files # 注意,cargo 會預設幫你建立 .git 檔,若已經存在有 .git 的專案底下則不會生成。 ``` ### `.toml` 檔 * 這個檔案其實就是這個專案的所有資訊(包括 dependency 以及版本等等) 先來打開檔案來瞧瞧 ```toml [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` :::warning 這邊要注意 cargo 專案中的程式碼都**必須**放在 `src` 目錄下 ::: 而在專案當中也只需要簡單的一行,cargo 即可幫忙把所有檔案連結起來,以下為範例 (cargo 在沒有更改原始碼的狀態下不會重新編譯,基本上就是全部都已經撰寫好的 `Makefile`) ```bash $ cargo build # 編譯並且 link $ cargo run # 編譯 + link + 跑程式 $ cargo check # 可以常常使用這個來確定你寫的 rust 程式碼可不可以過編譯,因為不是編譯,所以快很多。 ``` * [紀錄](https://users.rust-lang.org/t/cargo-build-where-the-executable-file-main/21818),在 cargo build/run 之後找不到可執行檔,首先要看 toml 裡面的 name,接下來,通常可執行檔的位置會存放在 `<project>/target/debug/` 這個目錄底下。 # Lesson-2 實作猜數字遊戲以及補充語法說明 根據[此教程](https://doc.rust-lang.org/book/ch02-00-guessing-game-tutorial.html)實作出一個猜數字遊戲。我覺得直接看程式碼來理解 rust 會比較快 ```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 行。以下來稍微解釋這幾行到底做了些甚麼 ```rust let mut guess = String::new(); ``` Rust 中存在著==可變動以及不可變動==的變數,`let` 宣告一個變數,而 `mut` 讓這個變數可以在後續被修改,而 `=` 把後面的資訊綁定給這個關鍵字。廢話不多說,馬上來實驗看看,以下為例子: ```rust= let a = 1; a = 2; ``` 可以看到第一行 `let` 定義了一個變數為 `a`,而這個變數在第二行的時候我嘗試賦值給他,接下來使用 `cargo check` 就會跑出以下錯誤訊息: ```bash 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` 替換成原本的引數(在這個情況下會出問題) :::info 這個會在 Lesson-4 中提到 ::: 再來看到最後的 `.except()`,手冊上提到這個是用來除錯使用的程序,當做完 `read_line` 之後,內部會有一個 Result,這個 Result 主要會有兩大項目,一個是 `OK`,另外一個是 `Err`(作用有點類似 [Errno](https://man7.org/linux/man-pages/man3/errno.3.html)) :::info 最後就是 std output,這邊不多敘述,因為 `{}` 的作用與 C 語言中針對型別的 `%d, %ld ...` 差不多。 ::: > 額外補充一點,`cargo` 這個工具還可以直接把當前 `dependency` 中的所有文件利用網頁顯示給你看(包括範例)只需要在當前目錄下使用 `cargo doc --open`,如此一來所有資訊都會在你面前顯示,這樣也不用擔心不知道要怎麼用。 接下來就是比較的時間了,以下程式碼為官方文件的程式碼。 ```rust // 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__` 讓我們可以在各種型別做不同的轉換,[例如](https://leetcode.com/problems/multiply-strings/): ```python def string_mutiply(str1: str, str2: str): return str(int(str1) * int(str2)) ``` 如此在 C 語言非常困難的型別轉換,在這邊可以直接利用以下程式碼的 `: <type> = ***.parse()` 來轉換 ```rust let guess: u32 = guess.trim().parse().expect("Please type a number"); ``` 以上示範了怎麼樣用字串做轉型,等等 `Lesson3` 會細說,這邊只需知道拿來做轉換即可。 最後,我們當然須要讓使用者不停輸入,這樣才可以讓使用者不停的猜,直到猜對。那勢必要把迴圈拿出來用了,以下為 `loop` 的範例 ```rust loop { // code here } ``` 作用與 `while(1)` 是相同的。再來只需要當猜中數字的時候跳脫迴圈即可 ```rust= 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 的命名邏輯中,常數通常以 ==全大寫==、==中間以底線分開==,這兩個規則命名,當定義了常數例如: ```rust const THREE_HOURS_IN_SECOND: u32 = 60 * 60 * 3; ``` 編譯器會將這個結果 `10800` 給定義為常數,而執行時也不會真正執行三次乘法(浪費時間) :::info 將常數定義在好的位置有助於解釋程式碼以及加速開發 ::: #### 變數 variables 變數又分為可變動以及不可變動(先前已經提過),而跟 const 最大的不同在於 *shadowing*,有點類似重新宣告。 我們可以用以下程式碼來測試看看 var 跟 const 的差別 ```rust= 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` 讓我們用另外一個形式試試看 ```rust= 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 宣告清楚這個型別: ```rust let a = 1; // compiler 認定為有號 32 位元整數 let b = 3.12; // 有號 32 位元浮點數 let c: i32 = 1; let d: f32 = 3.12; ``` 上述的四行會做到一樣的事情,就看程式設計者想要怎麼設計了。 :::info 有很多寫法與 C 語言雷同,這邊不會贅述,以下只會給予不同型別宣告的範例 ::: ```rust= /* 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 ```rust fn function () {} ``` 到此為止應該會有人想問,為甚麼沒有回傳任何值,那是因為在 rust 當中並沒有 void function,在每一個 function 都必須要有一個箭頭定義他的回傳型別 ==比較特別的就是在一個花括號的 `scope` 當中,預設最後一個 `expression` 為回傳值== 所以可以製作出下方的特殊函式 ```rust= fn zero () -> i32 { return 0; } fn zero_no_return () -> i32 { 0 } ``` 這兩個函式做了完全一樣的事情,回傳 0,但第五行為這個花括號的 `scope` 當中最後一個 expression,所以預設為回傳值 :::info 也可以用這個方法來定義變數 ```rust let y: i32 = { let x: i32 = 15; 15 + 5 } ``` 同理,花括號當中最後一個 expression 為回傳值,所以 y 理當為 20 ::: ## Control Flow 終於來到怎麼樣撰寫 `if else` 了,接下來會介紹語法以及特殊寫法。 ```rust 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 是可以有回傳值的,我們馬上來應用一下(這是到目前為止我覺得最刺激最好玩的部分) ```ruby let condition = true; let variable = if condition {5} else {6}; ``` 這邊應該可以猜到會出現什麼樣的東西了 (可以利用這種定義方法做出寫出很好玩的 code) ```bash Running `target/debug/Lesson_3_types` 5 ``` ### `loop` `loop` 這個關鍵字定義了類似 `while true` 的行為,但又和後者不同,最特別的點在於可以回傳值,也就是在 break 後面接上一個要回傳的值,可以當作這個 `scope` 的 `return value` ```rust let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; ``` 再來另外一個特別的地方就是,`break` 也可以當作多層迴圈中用來控制流程的關鍵字,以下為例子: ```rust= 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](https://hackmd.io/@sysprog/c-control-flow),在其他語言若要跳脫多重迴圈,比較簡易的方法是使用 `goto, return` 或是用多個 `if else` 去管理,後者會導致寫出來的程式碼缺少了美感並且不容易懂。從這邊即可得知這個 `break` 以及 `label` 的設計意義了。並且從剛剛的教學可以看到,`break` 後面可以接一個回傳值,那在這種跳脫多層迴圈的狀況呢? 非常簡單,只需要在 `label` 後面放入回傳值即可 ```diff - break 'counting_up; + break 'counting_up 777; ``` 若把前面的第 13 行改為以上的樣子,就可以在這個 scope 當中回傳 777(不過還是記得要把它改成變數型式) ### `while` `for` 就與大部分程式語言一樣,當 `while` 後方的 condition 為真時,會不停的跑內部的行為 而 `for` 的語法反而與 `python` 雷同 ```rust let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } ``` 輸出如下: ```bash the value is: 10 the value is: 20 the value is: 30 the value is: 40 the value is: 50 ``` 如此方便的語法,可以協助我們爾後在不確定 array 長度時遍歷整個 array 這個 section 的最後,我想要講一下 `..` 的功能 ```rust for i in (1..100) { println!("{i}"); } ``` 這邊的 `..` 其實就是把頭尾之間的所有整數展開,幫我們更好定義了變數會走動的範圍。