位元詩人 [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 回傳的結果,根據不同的回傳值給予相對的的行為。我們將於後續章節介紹相關的概念。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。