循环表达式 Loop

Rust 支持四多循环表达式的写法, 下面列出它们的基本语法:

loop {
  block
}

for pattern in iterator {
  block
}

while condition {
  block
}

while let pattern = expr {
  block
}

每种写法都应各自的使用场景, 本节会依次介绍它们.

loop 循环

最简单的循环语句就是 loop { block }, 它相当于 C 语言中的:

while (true) {
  block
}

但是, Rust 单独引入了一个 loop 关键字来表示一个无限循环语句.

终止无限循环的方法也有几种:

  • break 表达式, 立即终止循环
  • return 表达式, 立即终止循环并退出当前函数
  • 抛出错误, 立即终止循环. 退出当前函数并将错误向上继续抛出
  • 抛出 panic, 当前线程直接终止
  • 调用 std::process::exit() 退出程序

使用 return 表达式终止循环的例子:

fn fibonacci(mut n: i32) -> i32 {
    debug_assert!(n >= 0);
    let mut x: i32 = 0;
    let mut y: i32 = 1;
    loop {
        (x, y) = (x + y, x);
        n -= 1;
        if n == 0 {
            return x;
        }
    }
}

fn main() {
    assert_eq!(fibonacci(1), 1);
    assert_eq!(fibonacci(2), 1);
    assert_eq!(fibonacci(10), 55);
}

如果 loop 循环的内部代码块执行时产生了错误 (Result<T, E>), 该错误又没有在代码块内部捕获, 而是将错误向函数调用处抛出了, 那么就会立即终止当前的循环. 下面的示例程序会尝试读取 shadow 文件, 但因为没有读取权限, 就会产生 io::Error, 进而终止整个循环:

use std::fs;
use std::io;
use std::thread;
use std::time::Duration;

fn read_files() -> io::Result<()> {
    let mut is_shadow = false;
    loop {
        let filepath = if is_shadow {
            "/etc/shadow"
        } else {
            "/etc/passwd"
        };
        // 当尝试读取 shadow 文件 (当前用户无权读取) 时, 会终止无限循环.
        let content = fs::read_to_string(filepath)?;
        assert!(!content.is_empty());
        thread::sleep(Duration::from_secs(1));
        is_shadow = !is_shadow;
    }
}

fn main() {
    let ret = read_files();
    assert!(ret.is_err());
    assert_eq!(ret.err().unwrap().kind(), io::ErrorKind::PermissionDenied);
}

使用 break 终止循环

break 表达式的语法如下:

break [LifeTime | Label] [Expression]

可以发现 break 表达式比在其它语言中要更为复杂, 它后面通常都留空, 只立即终止当前循环; 但也可以跟随标签(label) 或者表达式

fn main() {
    let mut count = 0;
    loop {
        count += 1;

        if count == 3 {
            println!("three");
        }

        if count == 5 {
            println!("five");
            break;
        }
    }
}

break 跳转到最外层循环

多层嵌套的循环语句, 可以使用 break Label 跳出来.

fn main() {
    let mut sum = 0;
    
    'outer:
    for i in 0..100 {
        for j in 0..i {
            sum += j;
            if i * j > 200 {
                break 'outer;
            }
        }
    }
    assert_eq!(sum, 560);

    #[allow(clippy::never_loop)]
    #[allow(unused_labels)]
    'outer_loop: loop {
        println!("Enter the outer loop");

        'inner_loop: loop {
            println!("Enter the inner loop");

            break 'outer_loop;
        }
    }
    println!("Leave the outer loop");
}

循环中使用 break 来返回值

loop 表达式也可以有返回值:

fn main() {
    let mut x: i32 = 0;
    let mut y: i32 = 1;
    let accum: i32 = loop {
        (x, y) = (x + y, x);
        if x > 30 {
            break x + y;
        }
    };
    assert_eq!(accum, 55);
}

代码块使用 break 来返回值

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    let result: i32 =
        'block: {
            if args.len() % 3 == 0 && args.len() % 5 != 0 {
                break 'block 1;
            }
            if args.len() % 3 != 0 && args.len() % 5 == 0 {
                break 'block 2;
            }
            3
        };

    assert_eq!(result, 3);
}

深入理解 break 表达式

先看一个基于 RustQuiz#20 修改的示例程序, 考虑考虑程序运行的结果是什么样的:

#![allow(clippy::never_loop)]
#![allow(unreachable_code)]
#![allow(clippy::unused_unit)]

fn break1() {
    loop {
        if (break { println!("1") }) {
            let _ = 1;
        }
    }
}

fn break1_expanded() {
    loop {
        if (break {
            println!("1");
        }) {
            let _ = 1;
        }
    }
}

#[rustfmt::skip]
fn break2() {
    loop {
        if break { println!("2") }
        {
            let _ = 2;
        }
    }
}

fn break2_expanded() {
    loop {
        if break () {
            println!("2")
        }

        // Another unused block
        {
            let _ = 2;
        }
    }
}

fn break3() {
    loop {
        if break println!("3") {
            let _ = 3;
        }
    }
}

fn noop() {}

fn main() {
    break1();
    break1_expanded();
    break2();
    break2_expanded();
    noop();
    break3();
}

上面的代码中, break1_expand() 函数是对 break1() 的重新格式化, 这样更容易阅读:

  • break { println!("1"); } 这个表达式作为 if 表达式的条件, 会优先被执行, 会打印出 1
  • 它执行的结果是 (), 所以 if 表达式中的条件不成立, if 表达式内的代码块不会被执行
  • 然后立即终止本循环

可以看一下它的 MIR 代码:

fn break1() -> () {
    let mut _0: ();
    let _1: ();
    let mut _2: std::fmt::Arguments<'_>;
    let mut _3: &[&str];
    let mut _4: &[&str; 1];
    scope 1 {
    }

    bb0: {
        _4 = const break1::promoted[0];
        _3 = _4 as &[&str] (PointerCoercion(Unsize));
        _2 = Arguments::<'_>::new_const(move _3) -> [return: bb1, unwind continue];
    }

    bb1: {
        _1 = _print(move _2) -> [return: bb2, unwind continue];
    }

    bb2: {
        return;
    }
}

const break1::promoted[0]: &[&str; 1] = {
    let mut _0: &[&str; 1];
    let mut _1: [&str; 1];

    bb0: {
        _1 = [const "1\n"];
        _0 = &_1;
        return;
    }
}

break2() 就更奇怪了, 它与 break() 相比, 只是少了一对小括号. break2_expanded() 是它的展开样式, 可以发现 if break () { xxx } 表达式是核心, break () 表达式返回值为空, 所以 if 表达式条件判断不成立, if 语句内的代码块不会被执行.

break2()break2_expanded() 的 MIR 代码如下:

fn break2() -> () {
    let mut _0: ();
    scope 1 {
    }

    bb0: {
        return;
    }
}

fn break2_expanded() -> () {
    let mut _0: ();
    scope 1 {
    }

    bb0: {
        return;
    }
}

可以看出来, 这两个函数其实什么都不会做的, 类似于 noop() 函数:

fn break3() -> () {
    let mut _0: ();
    let _1: ();
    let mut _2: std::fmt::Arguments<'_>;
    let mut _3: &[&str];
    let mut _4: &[&str; 1];
    scope 1 {

跳过当前循环 continue

在循环语句中使用 continue 来跳过当前循环中的后续代码, 继续执行下个循环.

continue 表达式的语法如下:

continue [Lifetime | Label ]

可以看到, continue 表达式也是支持标签的, 用于快速跳出多层循环嵌套.

先看一个例子, 展示 continue 的一般用法:

fn main() {
    for i in 1..20 {
        if i % 15 == 0 {
            println!("factor of 15");
            continue;
        }

        if i % 5 == 0 {
            println!("factor of 5");
            continue;
        }

        if i % 3 == 0 {
            println!("factor of 3");
        }
    }
}

下面的例子, 展示了如何使用 continue Label 跳出多层循环:

fn main() {
    let mut sum = 0;

    'outer:
    for i in 0..100 {
        for j in 0..i {
            sum += j;
            if i * j > 200 {
                continue 'outer;
            }
        }
    }
    assert_eq!(sum, 2087);
}

for 循环

for .. in 表达式用于遍历一个迭代器.

fn main() {
    for i in 1..10 {
        println!("{i}^2 = {}", i * i);
    }
}

默认情况下, for 在遍历一个集合时会使用 Iterator trait 的 into_iter() 方法. 除了这个方法之外, 还有另外两个方法:

  • iter() 以引用的方法遍历集合, 不改变集合中的值, 该容器接下来还可以被使用
  • into_iter() 从集合中解析出里面的数据, 一旦遍历完它, 该集合接下来不可再被使用, 相当于把这个集合 move 到了这个循环中
  • iter_mut() 以可变引用的方法遍历集合, 可以改变集合中的值, 该集合在接下来还可被使用

while 循环

while 的一般写法跟在 C/C++ 语言中没有多少差别, 当条件成立时, 就执行内部的代码块; 当条件不成立时, 就终止循环. 看一个小示例:

fn main() {
    let mut num = 1;
    while !(num % 3 == 0 && num % 5 == 0) {
        num += 1;
    }
    assert_eq!(num, 15);
}

另一个小示例, 猜数字:

use std::io;

fn read_number() -> i32 {
    let mut line = String::new();
    if io::stdin().read_line(&mut line).is_err() {
        0
    } else {
        line.trim().parse::<i32>().unwrap_or_default()
    }
}

fn main() {
    println!("Guess number!");
    while read_number() != 42 {
        println!("Try again");
    }
    println!("You have got it!");
}

while let 循环

while let 表达式用于支持模式匹配, 当匹配成功时, 会执行 while 语句内的代码; 如果匹配失败了, 就终止循环.

下面的示例程序展示了单链表的一种写法, 注意里面的 len() 函数和 debug_print() 函数, 它们展示了 while let 的用法:

#[derive(Debug, Clone)]
pub struct ListNode {
    pub val: i32,
    pub next: Option<Box<ListNode>>,
}

impl ListNode {
    #[must_use]
    pub fn len(head: &Option<Box<Self>>) -> usize {
        let mut node_ref = head;
        let mut count = 0;
        while let Some(node_box) = node_ref {
            node_ref = &node_box.as_ref().next;
            count += 1;
        }
        count
    }

    #[must_use]
    #[inline]
    pub const fn is_empty(head: &Option<Box<Self>>) -> bool {
        head.is_none()
    }

    #[must_use]
    pub fn from_slice(slice: &[i32]) -> Option<Box<Self>> {
        let mut head = None;
        for &val in slice.iter().rev() {
            head = Some(Box::new(Self {
                val,
                next: head,
            }))
        }
        head
    }

    fn debug_print(head: &Option<Box<Self>>) {
        print!("head: [ ");
        let mut node_ref = head;
        while let Some(node_box) = node_ref {
            let val: i32 = node_box.as_ref().val;
            print!("{val}, ");
            node_ref = &node_box.as_ref().next;
        }
        println!("]");
    }
}

fn main() {
    let list = ListNode::from_slice(&[1, 2, 3, 5, 8]);
    ListNode::debug_print(&list);
    assert_eq!(ListNode::len(&list), 5);
}

为什么引入 loop 表达式

上文已经介绍了 loop 和 while 表达式, 那么问题来了, 既然 loop { block } 就相当于 while true { block }, 那为什么 Rust 还要单独引入一个新的关键字呢? 像 C/C++ 这样的语言并不需要这样.

先看一个示例代码:

fn fibonacci(mut n: i32) -> i32 {
    debug_assert!(n >= 1);
    let mut x = 1;
    let mut y = 0;
    // 换成 while true 后就会编译失败.
    // while true {
    loop {
        (x, y) = (x + y, x);
        n -= 1;
        if n == 1 {
            return x;
        }
    }
}

fn main() {
    assert_eq!(fibonacci(10), 55);
}

上面的 fibonacci() 函数中, 如果把 loop 换成 while true, 就会编译失败.

这个是 rustc 编译器比较特殊的地方, 因为它支持 flow-sensitive analysis. 在 if/while 等支持条件判断的语句中, 它不会直接判断 condition 表达式的值是 true 还是 false; 它会假设条件的值既可以是 true, 也可以是 false, 然后继续分析 if/while 语句内部的代码块. 很显然, 上面的代码中, 如果把 loop 换成 while true, 当 rustc 编译器检查代码时, 它就会发现 fibonacci() 函数在不同的分支判断情况下可能返回不同类型的值, 而这是不被允许的.

遇到这样的情况, 就直接用 loop 表达式.

同样的, 看另一个示例, 它来自某个服务器端模块代码, 初始化好之后, 服务就开始一直运行下去了, 直到整个进程退出, run_loop() 函数的返回值类型很不常见, ! 表示 never type.

impl Server {
    pub async fn run_loop(&mut self) -> ! {
        loop {
            tokio::select! {
                Some(cmd) = self.dispatcher_receiver.recv() => {
                    if let Err(err) = self.handle_dispatcher_cmd(cmd).await {
                        log::error!("Failed to handle dispatcher cmd: {:?}", err);
                    }
                }
                Some(cmd) = self.server_ctx_receiver.recv() => {
                    self.handle_server_ctx_cmd(cmd).await;
                }
            }
        }
    }
}