Rust 的三種循環

楔子

我們常常需要重複執行同一段代碼,針對這種場景,Rust 提供了多種循環(loop)工具。一個循環會執行循環體中的代碼直到結尾,並緊接着回到開頭繼續執行。

而 Rust 提供了 3 種循環:loop、while 和 for,下面逐一講解。

loop 循環

我們可以使用 loop 關鍵字來指示 Rust 反覆執行某一段代碼,直到我們顯式地聲明退出爲止。

fn main() {
    loop {
        println!("hello world");
    }
}

這段代碼會不停地在終端中打印 hello world,我們只能使用 Ctrl + C 來終止這種陷入無限循環的程序。當然,Rust 提供了另外一種更加可靠的循環退出方式,可以在循環中使用 break 關鍵字來通知程序退出循環。

fn main() {
    let mut x = 1;  // x 可變
    loop {
        println!("hello world");
        if x == 5 {
            break;
        }
        // 注意 x 必須是可變的
        // 否則此處報錯
        x += 1;
    }
    /*
    hello world
    hello world
    hello world
    hello world
    hello world
     */
}

打印了五遍就停止了,沒什麼好說的。但 loop 循環還支持返回值,我們舉個例子:

fn main() {
    let mut x = 1;
    let y = loop {
        if x == 5 {
            // break 之後的值就是整個 loop 的返回值
            break x * 2;
        }
        x += 1;
    };
    println!("y = {}", y);  // y = 10
}

如果 break 後面沒有值,那麼整個 loop 返回的就是空元組:

fn main() {
    let mut x = 1;
    let y = loop {
        if x == 5 {
            break;
        }
        x += 1;
    };
    println!("y = {:?}", y);  // y = ()
}

需要說明的是,無論 break 後面有沒有分號,它都是整個 loop 循環的返回值。

既然是 loop 循環是一個表達式,那麼除了賦值給一個變量之外,肯定也可以作爲函數的返回值:

fn f() -> i32 {
    let mut x = 1;
    loop {
        if x == 5 {
            break x * 2;
        }
        x += 1;
    } // 此處結尾不可以有分號
}

fn main() {
    println!("{}", f());  // 10
}

注意 loop 循環的最後一定不能加分號,因爲加了就會變成語句,而語句不會返回任何內容。所以在 if 表達式的時候我們囉嗦了那麼多關於表達式、分號的內容,就是因爲這些概念在循環中同樣會體現。

下面的做法是錯誤的:

fn f() -> i32 {
    let mut x = 1;
    loop {
        if x == 5 {
            break x * 2;
        }
        x += 1;
    };  // 這裏加上了分號
}

我們一定不能這麼做,因爲這會讓 loop 循環變成語句,而下面又沒有內容了,因此函數 f 會默認返回空元組。而函數的返回值簽名是 i32,於是出現矛盾,造成編譯錯誤。那麼下面這個例子可以嗎?

fn f() -> i32 {
    let mut x = 1;
    loop {
        if x == 5 {
            // break 語句結尾有沒有分號
            // 並不重要
            break x * 2;
        }
        x += 1;
    }
    33
}

答案是依舊不行,因爲 loop 循環是一個表達式,而它下面還有表達式,違反了我們之前說的函數末尾只能有一個表達式的原則。但是有一個例外,相信你已經猜到了,就是當 loop 表達式返回元組的時候,那麼會忽略掉。

fn f() -> i32 {
    let mut x = 1;
    loop {
        if x == 5 {
            // 等價於 break;
            break ();  
        }
        x += 1;
    }
    33
}

此時是沒有問題的,以上就是 loop 循環。

while 循環

另外一種常見的循環模式是在每次執行循環體之前都判斷一次條件,如果條件爲真,則執行代碼片段,如果條件爲假、或在執行過程中碰到 break 就退出當前循環。

這種模式可以通過 loop、if、else 及 break 關鍵字的組合使用來實現,有興趣的話可以試着完成這一功能。不過由於這種模式太過於常見,所以 Rust 爲此提供了一個內置的語言結構:while 條件循環。

fn main() {
    let mut x = 1;
    while x <= 5 {
        println!("hello world");
        x += 1;
    }
}

執行完之後會打印 5 次 hello world,然後是返回值的問題,while 循環不可以像 loop 一樣 break 一個值,也就是說它只能默認返回空元組。

fn f() -> i32 {
    let mut x = 1;
    while x <= 5 {
        if x == 3 {
            break;
        }
        x += 1
    }
    // 沒有下面這個 33,那麼該函數就是非法的
    33
}

fn main() {
    println!("{:?}", f());  // 33

    // 當然 while 循環也可以賦值給一個變量
    // 因爲只可能返回空元組,所以這麼做沒有什麼意義
    let x = while 1 <= 2 {
        break;
    };
    println!("{:?}", x);  // ()
}

而當 break 後面有值的時候,會編譯錯誤,假設我們 break 123。

告訴我們帶有值的 break 只能出現在 loop 循環中,而 while 循環是不支持的。另外即便 break 一個空元組也是不允許的,儘管 while 循環會默認返回空元組。

for 循環

我們遍歷一個數組可以選擇 loop 循環、while 循環,但是這樣容易因爲使用了不正確的索引長度而使程序崩潰。

fn traverse1() {
    let arr = [1, 2, 3, 4, 5];
    let mut sumi32 = 0;
    let mut indexusize = 0;
    loop {
        if index < 5 {
            // 通過索引獲取元素
            // 索引必須是 usize 類型
            sum += arr[index];
        } else {
            break;
        }
        index += 1;
    }
    println!("sum([1, 2, 3, 4, 5]) = {}", sum);
}

fn traverse2() {
    let arr = [1, 2, 3, 4, 5];
    let mut sumi32 = 0;
    let mut indexusize = 0;
    while index < 5 {
        sum += arr[index];
        index += 1;
    }
    println!("sum([1, 2, 3, 4, 5]) = {}", sum);
}

fn main() {
    traverse1();  
    // sum([1, 2, 3, 4, 5]) = 15
    traverse2();  
    // sum([1, 2, 3, 4, 5]) = 15
}

雖然成功遍歷了,但如果索引越界的話就會發生錯誤,因此可以使用 for 循環這種更簡明的方法來遍歷集合中的每一個元素。

fn traverse() {
    let arr = [1, 2, 3, 4, 5];
    let mut sumi32 = 0;
    for element in arr {
        sum += element;
    }
    println!("sum([1, 2, 3, 4, 5]) = {}", sum);
}

fn main() {
    traverse();  
    // sum([1, 2, 3, 4, 5]) = 15
}

結果是一樣的,但我們增強了代碼的安全性,不會出現諸如越界訪問或漏掉某些元素之類的問題。

假如後期修改代碼,我們從 arr 數組中移除了某個元素,卻忘記將循環中的條件更新爲 while index < 4,那麼再次運行代碼就會發生崩潰。而使用 for 循環的話,就不需要時常惦記着在更新數組元素數量時,還要去修改代碼的其他部分。

for 循環的安全性和簡捷性使它成爲了 Rust 中最爲常用的循環結構,即便是爲了實現循環特定次數的任務,大部分的 Rust 開發者也會選擇使用 for 循環。我們可以配合標準庫中提供的 Range 來實現這一目的,它被用來生成從一個數字開始到另一個數字結束之前的所有數字序列。

fn main() {
    for number in 1..4 {
        println!("number = {}", number);
    }
    /*
    number = 1
    number = 2
    number = 3
     */

    // 還可以逆序輸出
    for number in (1..4).rev() {
        println!("number = {}", number);
    }
    /*
    number = 3
    number = 2
    number = 1
     */
}

代碼是不是更加精煉了呢。

小結

以上就是 Rust 的循環,和其它語言相比沒太大區別,但多了一個支持返回值的 loop,在賦值的時候會方便很多。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/5oag57k5gn6ADUb_c59_Mg