Rust 基礎

我們直接打開 main.rs 來寫我們的程式吧,首先 // 開頭的是程式的註解,它是給人看的,電腦看到會直接忽略,我直接使用註解來說明程式的內容,希望你可以照著程式碼自己打一遍,這樣做相信會比較有印像,當然,註解的部份可以不用照著打,你也可以用你自己的方式用註解做筆記。請照著底下的內容輸入:

// 宣告使用外部的函式庫 rand
// 這告訴我們的編譯器我們要使用 rand 這個套件
extern crate rand;

// 引入需要的函式,如果不引入的話我們就需要在程式中打出全名來,
// 比如像下面使用到的 thread_rng 的全名就是 rand::thread_rng ,
// 但這裡我們選擇引入 rand::prelude::* 這是個比較方便的寫法,
// 很多套件的作者為了使用者方便,都會提供個叫 prelude 的模組,
// 讓你可以快速的引入必要的函式,我們要使用的 thread_rng 也有包含在裡面,
// 但並不是每個套件作者都會這麼做,請注意。
use rand::prelude::*;

// 這是標準輸入,也就是來自鍵盤的輸入,我們等下要從鍵盤讀玩家的答案。
use std::io::stdin;

// 這是一備函式,函式就是一段的程式,
// 我們可以在一個程式裡根據不同的功能將程式拆成一個個的函式,
// 不過今天這個程式並不大,我們直接全部寫在 main 這個函式裡就好了,
// main 是個特殊的函式, Rust 的程式都會從 main 開始執行。
fn main() {
    // 我們在這定義了一個變數 ans 來當作我們的答案,
    // 將它設定成 1~100 之間的隨機數字
    let ans = thread_rng().gen_range(1, 100);

    // 這邊又定義了兩個變數,分別代表答案所在的上下範圍,
    // 之後我們要把這個範圍做為提示顯示給玩家,
    // 因為之後需要修改這兩個變數的值,所以這邊必須加上 mut 來表示這是可修改的
    let mut upper_bound = 100;
    let mut lower_bound = 1;

    // 這是迴圈,它會重覆的執行包在這裡面的內容,
    // 因為這邊的迴圈沒有條件,所以它會一直反覆的執行,
    // 直到執行到如 break 才會結束,
    // 等下還會介紹另外兩種有條件的迴圈
    loop {
        // 這邊要建一個用來暫放玩家輸入的答案用的變數,
        // String 是個存放一串文字用的型態,也就是字串型態,
        // String::new 會建立一個空的字串
        let mut input = String::new();

        // 這邊要印出提示使用者輸入的顯示,同時我們也印出答案所在的上下界,
        // println! 在印完會自動的換行,也就是接下來的輸入輸出會從下一行開始,
        // 而裡面的 {} 則是用來占位子用的,分別是我們要印出上下界的位置,
        // 之後傳給 println! 的變數就會被放在這兩個位置
        println!(
            "答案在 {}~{} 之間,請輸入一個數字",
            lower_bound, upper_bound
        );

        // 這邊我們使用 read_line 從鍵盤讀入一整行進來,
        // 也就是到玩家按下 Enter 的字都會讀進來,
        // 讀進來的文字會被放進 input 裡,
        // 而因為放進 input 代表著修改 input 的內容,
        // 所以這邊比較特別一點,我們要加上 &mut 來允許 read_line 修改 input ,
        // 而 read_line 會除了把輸入放進 input 外也會傳回是否有成功讀取輸入,
        // 於是這邊就使用了 expect 來處理,若回傳的值代表錯誤時,
        // expect 會印出使用者傳給它的訊息並結束掉程式
        stdin().read_line(&mut input).expect("Fail to read input");

        // trim() 會把字串前後的空白字元 (空格與換行) 去掉,
        // 而 parse::<i32>() 則是把字串從原本的文字型態轉換成數字,
        // 這樣我們在之後才可以拿它來跟答案做比較,
        // 我們這邊又重新定義了一次 input 來放轉換成數字後的結果,
        // 如果你有學過其它的語言可能會覺得奇怪,為什麼允許這麼做,
        // 這也是 Rust 一個有趣的地方, Rust 允許你重覆使用同一個變數名稱,
        // parse 也是回傳代表正確或錯誤的 Result 不過這次我們不用 expect 了,
        // 這次我們判斷是不是轉換失敗,如果是則代表玩家輸入了不是數字的東西,
        // 那我們就讓玩家再輸入一次, match 是用來比對多個條件的語法,之後
        // 會有一篇來介紹這個語法,因為它是 Rust 裡一個很強大的功能。
        let input = match input.trim().parse::<i32>() {
            // Ok 代表的是正確,同時它會包含我們需要的結果
            // 因此這邊把轉換完的數字拿出來後回傳
            // Rust 裡只要沒有分號,就會是回傳值
            Ok(val) => val,
            // Err 則是錯誤,它會包含一個錯誤訊息,不過我們其實不需要,
            // 這邊我們直接提示使用者要輸入數字並結束這次迴圈的執行
            Err(_) => {
                println!("Please input a number!!!");
                // continue 會直接跳到迴圈的開頭來執行,也就是 loop 的位置
                continue;
            }
        };

        // 這邊使用 if 來判斷玩家的答案跟正確答案是不是一樣,
        // if 會判斷裡面的判斷式成不成立,如果成立就執行裡面的程式,
        // 要注意的是判斷相等是雙等號,因為單個等於已經用在指定了。
        if input == ans {
            println!("恭喜你,你答對了");
            // break 則會直接結束迴圈的執行,
            // 於是我們就可以離開這個會一直跑下去的迴圈
            break;
        // 如果不一樣,而且玩家的答案比正確答案大的話就更新答案的上限
        } else if input > ans {
            upper_bound = input;
        // else 會在 if 的條件不成立時執行,並且可以串接 if 做連續的判斷,
        // 像上面一樣。都不是上面的情況的話就更新下限
        } else {
            lower_bound = input;
        }
    }
}

變數宣告

定義一個變數的語法是像這樣的:

let ans: i32 = 42;

變數是用來存放程式的資料用的,等號在這邊是指定的意思,並不是數學上的相等,我們把 42 指定給 ans 這個變數,而因為 42 是一個數字,因此我們把 ans 這個變數指定為 i32 這個整數型態,然而事實上大部份情況下其實是不需要給型態的,因為 Rust 可以自動的從初始值,也就是你一開始指定的值推導出你的變數的型態。

在 Rust 裡預設變數是不可以修改的,一但給了值就不能改變,如果要能修改就必須加上 mut ,這可以讓你在之後還能修改變數的值。

錯誤的範例:

let ans = 42;
ans = 123; // 這邊重新指定了值給 ans ,但 ans 並沒有宣告為可改變的

正確的範例:

let mut ans = 42;
ans = 123;

在 Rust 中的變數都必須要有初始值,如果沒有初始值會是一個錯誤,當然,你可以先宣告再給值,但只要有可能會發生沒有指定初始值的情況就會發生錯誤。

錯誤的範例:

let ans;
if true {
    ans = 42;
}
// 這邊我們沒有 else 的情況就使用了,所以 Rust 認為 ans 可能沒有初始值
println!("{}", ans);

正確的範例:

let ans;
if true {
    ans = 42;
} else {
    ans = 0;
}
println!("{}", ans);

基本型態

在電腦裡資料都有其型態,電腦必須知道資料屬於哪一種才能做出正確的處理,而 Rust 的基本型態則有:

以下為了說明都有在定義變數時把型態寫出來,但平常因為可以自動推導,所以並沒有必要寫出來。

  • unit: 型態是 () ,同時值也是 () ,這是一個代表「無」的型態
//  就是「無」,你沒辦法拿這個值來做什麼
let unit: () = ();
  • 布林值 bool: 值只有真 true 與假 false ,一個代表真假的型態,同時也是判斷式所回傳的型態
let t: bool = true;

// if 判斷的結果一定要是布林值,其實所有的判斷式的結果也都是布林值
if t {
    println!("這行會印出來");
}

if false {
    println!("這行不會印出來");
}
  • 整數: 上面使用的 i32 就是整數,實際上整數家族有具有正負號的 i8i16i32i64i128 與只能有正整數的的 u8u16u32u64u128 它們之間的差別在有沒有正負號,以及能存的數字最大的大小,平常通常只會用到 i32 ,先只要記得 i32 就行了
let num: i32 = 123;
// 數字你可以做基本的四則運算:加法 (+) 、減法 (-) 、乘法 (*) 、除法 (/)
// 和 取餘數 (%) ,當然也還有比如取絕對值之類的方法在,不過這邊先不提
println!("1 + 1 = {}", 1 + 1);
println!("10 % 3 = {}", 10 % 3);
  • 浮點數: 就是小數,有 f32f64 ,但平常只會用到 f64 ,浮點數也可以像整數一樣做計算,只是無法取餘數
let pi: f64 = 3.14;
  • 字串: String ,代表的是一串的文字,另外還有切片型態的 str (這兩個詳細的差別之後再提) ,如果你要讀使用者的輸入或是要能夠修改內容的要用 String ,如果要放固定的字串 (比如顯示的訊息) 用 str
let message: &str = "這是個固定的訊息";
println!("{}", message);
let mut s: String = String::from("可以用 from 來建一個 String");
s.push_str(",同時你也可以增加內容在 String 的結尾");
println!("{}", s);
  • 字元: char 一個字就是一個字元,比如 'a' 或是 '三' 也是
let c1: char = '中';
let c2: char = '文';
let c3: char = '也';
let c4: char = '行';

這邊為了說明都有在變數後加上型態,如果平常沒加型態而是讓編譯器自動推導的話,整數預設會使用 i32 ,符點數會使用 f64 ,但若之後把變數傳入了其它函式,則會配合那個函式的需求改變。

複合型態

  • 陣列 (array) :由一串相同型態與固定長度的資料組成的
// i32 是元素的型態, 4 則是長度
let mut array: [i32; 4] = [1, 2, 3, 4];

// 這邊印出第一個數字與最後一個數字,陣列的編號是從 0 開始的
println!("{}, {}", array[0], array[3]);

// 我們也可以修改元素的值,同樣的,請注意上面的定義是有加 mut 的
array[1] = 42;
  • 元組 (tuple):可由不同的型態組成
// 元組的型態要把每個欄位的型態都寫出來
let mut tuple: (i32, char) = (42, 'a');

// 印出第一個值
println!("{}", tuple.0);

// 改變第二個值
tuple.1 = 'c';

條件判斷 if

// if 裡面要放條件判斷,或是布林值,
// 條件判斷除了上面出現的 == 、 > 、 < 外還有個不等於
if 10 != 5 {
    println!("10 != 5");
}

如果要判斷多個條件可以用底下的方式串起來:

  • && :「且」,兩邊都成立時才成立
  • || :「或」,其中一邊成立就成立
if 2 > 1 && 3 > 2 {
    println!("2 > 1 且 3 > 2");
}

還有 ! 可以把判斷式的結果反轉,也就是把布林值的 truefalsefalsetrue

if !false {
    println!("這行會印出來");
}

另外你可以用 if 來根據條件來指定變數:

let ans = if true {
// 請注意,這邊沒有分號
    42
} else {
    123
}; //  這邊要加分號

Rust 裡只要不加分號就會變回傳值

while

while 是有條件的迴圈,只有當條件滿足時才會繼續執行下去:

let mut i = 0;
while i < 5 {
    println!("i = {}", i);
    // 這是 i = i + 1; 的縮寫,數學運算都可以這樣寫
    if i == 3 {
        // 我們在 i 為 3 時就結束迴圈了,所以你會看到從 0 印到 3
        break;
    }
    i += 1;
}

break 可以用來中斷迴圈,而 continue 可以直接結束這次的迴圈,跳到迴圈的開頭執行。

for

for 是用來跑過一個「範圍」的資料的,比如像陣列,以後會再詳細介紹背後的機制。

for item in [1, 2, 3, 4, 5].iter() {
    println!("{}", item);
}

for item in 0..5 {
    println!("{}", item);
}

其中 0..5 是 Rust 的 range 代表的是 0~4 (不含結尾),含結尾的話要寫成 0..=5 (有等號),這代表一個數字的範圍,以後講到切片時會再提到。

函式

上面說了函式就是一小段的程式,如果你的程式裡出現了重覆的程式碼,你可以試著把程式碼抽出來變成函式,這樣以後要修改也會比較方便。

函式來自於數學的函數的觀念「給予一個值,會對應到另一個固定的值」,函式同樣的也需要輸入的值與輸出的值,底下是個範例:

// 這個函式有兩個整數的輸入值 a 與 b 並且回傳一個整數
// 函式的開頭是 fn 接下來跟著函式的名字,後面的括號裡放著函式的輸入
// 其中當作輸入的鑾數都一定要有型態,之後 -> 後放著的是回傳的型態
fn add(a: i32, b: i32) -> i32 {
    // 這邊示範如何使用 return ,如果 a 與 b 都是 1 ,就直接回傳 2
    if a == 1 && b == 1 {
        // return 會提早結束函式的執行,並且把後面的值當成回傳值
        return 2;
    }
    // 注意這邊沒有括號,沒有括號的代表回傳值,當然你也可以像上面使用 return
    a + b
}

// 這個函式沒有寫出回傳值,這代表它其實會回傳一個 () ,只是可以省略不寫出來而已
fn print_number(num: i32) {
    println!("{}", num);
}

// 所以其實 main 函式回傳的也是 unit
fn main() {
    // 像這樣子就可以呼叫函式了
    let num = add(1, 2);
    print_number(num);

    // 你也可以寫在一起
    print_number(add(1, 2));
}

這篇我們很快的介紹了 Rust 的基本語法,下一篇要介紹的是 Rust 的參考,以及 Rust 中很重要的變數的所有權的觀念。