[Rust] 程式設計教學:控制結構 (Control Structure) 或控制流程 (Control Flow)

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    前言

    到目前為止,我們的程式都是由上往下依序執行。透過控制結構 (control structure) 或控制流程 (control flow),可以藉由改變程式執行的順序而達到我們所預期的行為。

    控制結構分為兩種:

    • 選擇結構 (selection structure) 或條件敘述 (conditional)
    • 迭代結構 (iteration structure) 或迴圈 (loop)

    我們將在本文中介紹這些控制結構。

    條件敘述 (Conditional)

    條件敘述的作用在區隔程式碼區塊,符合特定條件時,才執行特定區塊內的程式碼,使得程式可以依不同情境改變其行為。Rust 的條件控制有 ifmatch 兩種。

    if

    最常用的條件敘述為 if,其想法如以下流程圖:

    if 區塊

    當我們把程式碼包在 if 區塊內,只有符合 if 區塊的條件時,才會執行該區塊內的程式碼。所以 if 是選擇選構。

    if 寫成虛擬碼如下:

    if condition {
        statement
    }
    

    虛擬碼 (pseudo code) 是一種介於文字敘述和程式碼間的程式表達方式,在撰寫程式碼前,可先使用虛擬碼整理流程,其好處在於可用抽象的方式表達程式的執行步驟。虛擬碼沒有固定的撰寫方式,有些會寫得很像文字,有些會寫得很像數學公式或程式碼。在撰寫虛擬碼時,會先省略程式碼的實作細節,專注在抽象思路上。

    以下是一個實例:

    fn main() {
        let x = 3;
     
        if x > 0 {
            println!("x is larger than zero");
        }
    }
    

    if 還可以選擇性加入 else 區塊,這時候變成二元敘述。其想法如以下流程圖:

    if-else 區塊

    將此想法寫成虛擬碼如下:

    if condition {
        statement 1
    } else {
        statement 2
    }
    

    以下是一個簡短的實例:

    fn main() {
        let x = 3;
     
        if x > 10 {
            println!("x is larger than 10");
        } else {
            println!("x is not larger than 10");
        }
    }
    

    如果有更複雜的條件,可以用 if-else if-else 區塊,其想法如下:

    if-else if-else 區塊

    寫成虛擬碼如下:

    if condition 1 {
        statement 1
    } else if condition 2 {
        statement 2
    } else {
        statement 3
    }
    

    其中 else if 區塊可視需要重覆多次,而最後的 else 區塊可省略。這時候 if 成為多元敘述。

    以下是一個實例:

    fn main() {
        let x = 0;
     
        if x > 0 {
            println!("x is larger than zero");
        } else if x == 0 {
            println!("x is equal to zero");
        } else {  // x < 0
            println!("x is smaller than zero");
        }
    }
    

    if 區塊是表達式而非敘述

    if 也可以作為回傳值,如下例:

    fn main() {
        let score = 83;
     
        let grade = if score > 90 {
            "A"
        } else if score > 80 {
            "B"
        } else if score > 70 {
            "C"
        } else if score > 60 {
            "D"
        } else {
            "F"
        };
     
        assert_eq!(grade, "B");
    }
    

    利用 if 回傳值時,要注意兩點。首先,回傳值不要加分號 ;,第二,if 區塊後要加分號。在 Rust 中,沒加分號的程式碼為表達式 (expression),而有加分號的程式碼為敘述 (statement)。在本程式中,”A”、”B” … “F” 皆為回傳值,視為表達式,if 區塊本身也為表達式。除了回傳值以外,程式碼都要寫成敘述,故最後需再加上分號。

    match

    對於多個 if-else if-else,可以用 match 簡化。範例如下:

    fn main() {
        let grade = "A";
     
        match grade {
            "A" => println!("Wonderful"),
            "B" => println!("Good"),
            "C" => println!("So so"),
            "D" => println!("It could be better"),
            "F" => println!("Oh, oh"),
            _ => println!("Unknown grade"),  // Error on removal.
        }
    }
    

    雖然 Rust 的 match 類似其他語言的 switch,但是 match 要窮舉所有可能的情形,否則會引發錯誤。像是下列看似正常的程式:

    fn main() {
        let grade = "A";
     
        match grade {
            "A" => println!("Wonderful"),
            "B" => println!("Good"),
            "C" => println!("So so"),
            "D" => println!("It could be better"),
            "F" => println!("Oh, oh"),
        }
    }
    

    卻引發了以下錯誤:

    error[E0004]: non-exhaustive patterns: `&_` not covered
    

    通常,就是在最後一個條件加上一個底線 _,作為啞變數,代表所有其他未符合的情形即可。

    match 要窮舉所有情境的設計是安全上的考量,以免程式設計者遺漏一些未考慮的情境。這是 Rust 在安全上的考量。

    match 是表達式而非敘述

    matchif 相同,也可以做為回傳值。如下例:

    fn main() {
        let char = 'c';
     
        let sound = match char {
            'A' | 'a' | 'E' | 'e' | 'I' | 'i' | 'O' | 'o' | 'U' |'u' => "vowel",
            'A' ... 'Z' | 'a' ... 'z' => "consonant",
            _ => "other"
        };
     
        assert_eq!(sound, "consonant");
    }
    

    在本程式中,只要符合 vowel 的,都會回傳 vowel,所以,我們在第二個條件以所有的英文字母為條件也沒關係,符合 vowel 的部分已排除。

    迴圈 (Loop)

    迴圈的作用在重覆,符合特定條件時,重覆執行某區塊的程式碼,減少輸入重覆的程式碼。Rust 的迴圈有 loopwhilefor

    loop

    loop 是最簡單的一種迴圈,其想法如下:

    loop 迴圈

    一進入 loop 迴圈後,就不間斷地反覆執行區塊內的程式碼,也就是俗稱的無窮迴圈。無窮迴圈可能是迴圈條件沒寫好所造成的 bug,不過,某些程式也是會用到無窮迴圈,像是 遊戲引擎就是一個很大的無窮迴圈。通常無窮迴圈會搭配中斷敘述,在本章後面會提到相關的內容。

    如果寫成虛擬碼則是以下形式:

    loop {
        statement
    }
    

    以下是程式範例:

    fn main() {
        loop {
            println!("Hello, World");
        }
    }
    

    註:讀者執行此程式後,可按 Ctrl + c 中斷此程式。

    while

    while 在程式滿足終止條件前,會不間斷地執行該區塊內的程式碼。其想法如下:

    while

    我們在使用 while 時,會在其區塊內加入改變程式狀態的程式碼,否則,就變無窮迴圈了,這通常不會是我們期待的效果。

    若寫成虛擬碼,其形式如下:

    while condition {
        statement
    }
    

    以下是實例:

    fn main() {
        let mut count = 10;
     
        while count > 0 {
            println!("Count down {}", count);
            count -= 1;
        }
    }
    

    以下的 while 迴圈和 loop 迴圈等價:

    fn main() {
        while true {
            println!("Hello, World");
        }
    }
    

    但是,Rust 官方手冊有提到,如果在程式碼中明確想用無窮迴圈時,loopwhile true 來得好,這會影響到程式的優化。

    for

    for 迴圈和 while 迴圈不同,for 會有明確的執行次數。Rust 的 for 迴圈的想法如下:

    Rust 的 for 迴圈

    在這個圖中,迭代器 (iterator) 是一個相對陌生的概念。首先,要知道容器 (collection) 的概念,容器用來存放資料,程式設計者可操作容器,藉此處理資料。以下是一個假想的線性容器:

    容器

    讀者可以想像得到,對於不同的容器,走訪其內部的資料的方式各自不同。透過迭代器,使用者可以在不知道容器內部實作的情形下,走訪某個容器中所有的資料。for 迴圈會自動走訪迭代器,並在迭代器結束時中斷迴圈。

    以虛擬碼的形式表示如下:

    for variable in iterator {
        statement
    }
    

    在 Rust 中,透過 range 即可提供迭代器。假設我們要從 1 數到 10,用 range 會寫成 1..11,其中包括起始點 1,但不包括結尾點 11。以下為實例:

    fn main() {
        // Iterate from 1 to 10
        for i in 1..(10+1) {
            println!("{}", i);
        }
    }
    

    如果我們不需要使用迭代器的計數,只要執行特定的次數,可使用啞變數,實例如下:

    fn main() {
        for _ in 1..(10+1) {
            println!("Hello, World");
        }
    }
    

    如果我們的計數不是以 1 遞增呢?現階段來說,要使用一些函數式程式設計的方法。實例如下:

    fn main() {
        for i in (1..).step_by(2).take(5) {
            println!("{}", i);
        }
    }
    

    這段程式碼意思是先建立一個從 1 開始的無限長序列 (1..),每兩個數字就取出一次 .step_by(2),共取出 5.take(5)

    不習慣函數式程式設計的讀者可能會感到不可思議,為什麼可以建立無限長度的序列。這牽涉到 Rust 編譯器內部的實作,我們這裡就不詳談,只要知道函數式程式設計有這項特性即可。

    如果還不習慣函數式程式,可以先用等效的 while 迴圈代替,範例如下:

    fn main() {
        let mut i = 1;
        while i < 10 {
            println!("{}", i);
            i += 2;
        }
    }
    

    改變迴圈運作

    我們看一下以下的範例:

    fn main() {
        let mut i = 1;
        let mut flag = false;
     
        while !flag {
            println!("{}", i);
            i += 1;
     
            if i > 5 {
                flag = true;
            }
        }
    }
    

    在這個程式中,我們使用變數 flag 搭配 while 迴圈來控制迴圈的運作。這裡的 flag 只是為了控制迴圈行進方向而用的變數,和程式的主邏輯無關。

    除了利用旗標 (flag) 表示程式狀態外,Rust 提供 breakcontinue 這兩個關鍵字,使得迴圈的控制更簡潔,而不需要額外的旗標。

    break 的作用是中斷迴圈,通常會寫在條件敘述內。以上的範例可以改寫如下:

    fn main() {
        let mut i = 1;
     
        loop {
            println!("{}", i);
            i += 1;
     
            if i > 5 {
                break;
            }
        }
    }
    

    continue 不會中斷迴圈,但會跳過同一個迴圈內該指令之後的程式碼。範例如下:

    fn main() {
        for i in 1..11 {
            if i % 2 == 0 {
                continue;
            }
     
            println!("{}", i);
        }
    }
    

    迴圈標籤

    對於較複雜的迴圈,如果想要精確地控制迴圈的運作,可用 breakcontinue 搭配迴圈標籤 (loop label)。範例如下 (摘自 Rust 官方文件):

    fn main() {
        'outer: for x in 0..10 {
           'inner: for y in 0..10 {
                if x % 2 == 0 { continue 'outer; } // continues the loop over x
                if y % 2 == 0 { continue 'inner; } // continues the loop over y
                println!("x: {}, y: {}", x, y);
            }
        }
    }
    

    (案例選讀) 終極密碼

    我們以終極密碼這個常見的遊戲做為本節的案例。本遊戲採用以下的規則:給定某個特定範圍,隨機選定一個數字。玩家試著猜這個數字,如果猜對,遊戲就結束,否則,就繼續猜。

    將我們本案例的想法以虛擬碼表示如下:

    Set the minimum and the maximum.
     
    Set a random answer between the minimum and the maximum.
     
    loop {
        Receive the guess from user.
     
        If the guess is correct, leave the loop.
    }
    

    我們做了一些小改良,在以下範例中,會判斷使用者的輸入值,限制使用者輸入的值在一個合理的範圍內。在本範例中,為了要產生亂數,我們使用 rand 套件,要修改 Cargo.toml ,加入以下內容:

    [dependencies]
    rand = "0.3"
    

    這裡附上範例程式碼,僅供參考:

    // Call rand package for random number generation
    extern crate rand;
     
    use std::io;
    use std::io::Write;
    use rand::Rng;
     
    fn main() {
        // Set the limit of answer
        const MAX: u32 = 100;
        const MIN: u32 = 1;
     
        // Set the limit of user input
        let mut upper = MAX;
        let mut lower = MIN;
     
        // Get a random number between MIN and MAX
        let answer = rand::thread_rng().gen_range(MIN, MAX + 1);
     
        loop {
            // Prompt for user input
            print!("Input a number ({}-{}):", lower, upper);
            // Flush standard out
            let _ = io::stdout().flush();
     
            // Receive user input
            let mut input = String::new();
            io::stdin()
                .read_line(&mut input)
                .expect("Failed to read from stdin");
     
            // Parse integer
            let guess = match input.trim().parse::<u32>() {
                Ok(n) => n,
                Err(_) => {
                    println!("Not a valid number");
                    continue;
                }
            };
     
            // Check the range of the guess
            if guess < lower || guess > upper {
                println!("The number should be between {} and {}",
                         lower, upper);
                continue;
            }
     
            // Check whether the guess is correct
            if guess == answer {
                println!("You got it!");
                break;
            } else if guess > answer {
                println!("Too large");
                upper = guess;
            } else {  // guess < answer
                println!("Too small");
                lower = guess;
            }
        }
    }
    

    由於從使用者接收到的輸入是字串,必需要轉型為整數後才能使用。字串轉換整數那段程式碼用到了 enum 和 generics 等新的概念。基本上,這段程式碼的意思是,接收 parse 回傳的結果,根據不同的回傳值給予相對的的行為。我們將於後續章節介紹相關的概念。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk
    標籤: CONDITIONAL, LOOP, RUST