此篇筆記主要以資訊工程研究所(在已經撰寫了幾個月的 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}");
}
```
這邊的 `..` 其實就是把頭尾之間的所有整數展開,幫我們更好定義了變數會走動的範圍。