Rust中各类循环介绍

238 阅读6分钟

for 循环的短路

假设我有一个u32s的Iterator 。我想把每个值加倍并打印出来。

fn weird_function(iter: impl IntoIterator<Item=u32>) {
    for x in iter.into_iter().map(|x| x * 2) {
        println!("{}", x);
    }
}

fn main() {
    weird_function(1..10);
}

现在我们假设我们讨厌数字8,想在碰到它时停止。这是一个简单的单行更改。

fn weird_function(iter: impl IntoIterator<Item=u32>) {
    for x in iter.into_iter().map(|x| x * 2) {
        if x == 8 { return } // added this line
        println!("{}", x);
    }
}

容易,完成,故事结束。出于这个原因,我建议尽可能地使用for 循环。尽管从函数式编程的背景来看,这感觉过于强制性了。然而,外面有些人想更多地使用函数式,所以让我们来探讨一下。

for_each vs map

让我们暂时忘记短路的问题。现在我们想回到程序的原始版本,但使用for 循环。用for_each 这个方法就很容易了。它需要一个闭合,为Iterator 中的每个值运行这个闭合。让我们来检查一下它。

fn weird_function(iter: impl IntoIterator<Item=u32>) {
    iter.into_iter().map(|x| x * 2).for_each(|x| {
        println!("{}", x);
    })
}

但是,我们到底为什么需要for_each ?这似乎与map 非常相似,后者也是Iterator 中的每一个值应用一个函数。然而,试图做这样的改变,表明了问题所在。使用这段代码。

fn weird_function(iter: impl IntoIterator<Item=u32>) {
    iter.into_iter().map(|x| x * 2).map(|x| {
        println!("{}", x);
    })
}

我们得到一个错误信息

error[E0308]: mismatched types
 --> src\main.rs:2:5
  |
2 | /     iter.into_iter().map(|x| x * 2).map(|x| {
3 | |         println!("{}", x);
4 | |     })
  | |______^ expected `()`, found struct `Map`

我不甘示弱,在表达式的末尾加上一个分号来解决这个错误。这就产生了一个警告:unused `Map` that must be used 。 果然,运行这个程序没有产生任何输出。

问题是,map 并没有耗尽Iterator 。换个说法,map懒惰的。它把一个Iterator 改编成一个新的Iterator 。但除非有什么东西出现并耗尽迫使 Iterator ,否则就不会发生任何行动。相比之下,for_each 总是会耗尽一个Iterator

强制耗尽一个Iterator 的一个简单技巧是使用count() 方法。这将执行一些不必要的工作,计算Iterator 中有多少个值,但它并不昂贵。另一种方法是使用collect 。这个有点棘手,因为collect 通常需要一些类型注释。但要感谢一个有趣的技巧,即FromIterator 是如何实现单位类型的,我们可以把一个()s的流收集到一个单一的() 值中。意思是说,这段代码是有效的。

fn weird_function(iter: impl IntoIterator<Item=u32>) {
    iter.into_iter().map(|x| x * 2).map(|x| {
        println!("{}", x);
    }).collect()
}

注意结尾处没有分号。如果我们加入分号,你认为会发生什么?

短路

EDIT足够多的人问 "为什么不使用take_while ?",我想我应该解决这个问题。是的,下面,take_while 将用于 "短路"。这甚至可能是个好主意。但这篇文章的主要目的是探索一些有趣的实现方法,而不是推荐一种最佳做法。而且总的来说,尽管有一些很好的论据证明take_while 是一个很好的选择,但我仍然坚持总体建议,即为了简单起见,更喜欢for 循环。

for 循环的方法,停在前8个是一个微不足道的,只需增加一行。让我们在这里做同样的事情。

fn weird_function(iter: impl IntoIterator<Item=u32>) {
    iter.into_iter().map(|x| x * 2).map(|x| {
        if x == 8 { return }
        println!("{}", x);
    }).collect()
}

猜测一下输出会是什么。准备好了吗?好的,这里是真实的情况。

2
4
6
10
12
14
16
18

我们跳过了8,但我们并没有停止。这就是continuebreakfor 循环内的区别。为什么会出现这种情况?

思考一下return 的范围是很重要的。它将退出当前函数。而在这种情况下,当前的函数不是weird_function ,而是 map 调用里面的闭包。这就是在map 内短路的困难之处。

同样的评论也适用于for_each 。阻止for_each 继续下去的唯一方法是惊慌失措(或者中止程序,如果你想变得非常激进)。

但是对于map ,我们有一些巧妙的方法来解决这个问题,并进行短路。让我们来看看它的作用。

收集一个Option

map 需要一些排泄方法来驱动它。我们一直在使用 。我collect之前已经讨论过这个方法的复杂性。 的一个很酷的特点是,对于 和 ,它提供了短路功能。我们可以修改我们的程序来利用这一点。collect Option Result

fn weird_function(iter: impl IntoIterator<Item=u32>) -> Option<()> {
    iter.into_iter().map(|x| x * 2).map(|x| {
        if x == 8 { return None } // short circuit!
        println!("{}", x);
        Some(()) // keep going!
    }).collect()
}

我把weird_function 作为返回类型,尽管我们也可以在collect 上使用turbofish,然后扔掉结果。我们只需要一些类型注释来说明我们要收集的东西。由于收集底层的() 值并不占用额外的内存,这甚至是相当有效的!唯一的代价是额外的 。唯一的代价是额外的Option 。但这个额外的Option (可以说)是有用的;它让我们知道我们是否短路了。

但是对于其他类型,情况就不那么乐观了。比方说,我们在map 内的闭包返回x 的值。换句话说,用Some(x) 替换最后一行,而不是Some(()) 。现在我们需要以某种方式收集这些u32s。

fn weird_function(iter: impl IntoIterator<Item=u32>) -> Option<Vec<u32>> {
    iter.into_iter().map(|x| x * 2).map(|x| {
        if x == 8 { return None } // short circuit!
        println!("{}", x);
        Some(x) // keep going!
    }).collect()
}

但这将产生一个我们不想要的堆分配!而且使用之前的 也是没用的。而使用之前的count() 也是没用的,因为它甚至不会短路。

但我们还有一个技巧。

事实证明,在Iterator 上还有一个可以执行短路的耗费方法:sum 。这个程序运行得非常好。

fn weird_function(iter: impl IntoIterator<Item=u32>) -> Option<u32> {
    iter.into_iter().map(|x| x * 2).map(|x| {
        if x == 8 { return None } // short circuit!
        println!("{}", x);
        Some(x) // keep going!
    }).sum()
}

缺点是它不必要地将数值相加。如果发生某种溢出,也许这可能是一个真正的问题。但这基本上是可行的。但是有没有什么方法可以让我们保持功能,短路,并且没有性能开销?当然可以!

短路

这里的最后一个技巧是创建一个新的辅助类型,用于求和Iterator 。但这个东西不会真的求和。相反,它将扔掉所有的值,并在看到Option 时立即停止。让我们在实践中看看。

#[derive(Debug)]
enum Short {
    Stopped,
    Completed,
}

impl<T> std::iter::Sum<Option<T>> for Short {
    fn sum<I: Iterator<Item = Option<T>>>(iter: I) -> Self {
        for x in iter {
            if let None = x { return Short::Stopped }
        }
        Short::Completed
    }
}
fn weird_function(iter: impl IntoIterator<Item=u32>) -> Short {
    iter.into_iter().map(|x| x * 2).map(|x| {
        if x == 8 { return None } // short circuit!
        println!("{}", x);
        Some(x) // keep going!
    }).sum()
}

fn main() {
    println!("{:?}", weird_function(1..10));
}

就这样!我们完成了!

练习在这里使用sum 是非常厚颜无耻的。collect 更有意义。用collect 替换sum ,然后把Sum 的实现改成别的东西。解决方案在最后。

结论

为了实现功能,这是个不小的工程。Rust有一个围绕短路的伟大故事。这不仅仅是指returnbreakcontinue 。它还包括? 尝试操作符,它是Rust中错误处理的基础。有些时候,你会想使用Iterator 适配器、异步流适配器和函数式代码。但除非你有迫切的需要,否则我的建议是坚持使用for 循环。

解决方案

use std::iter::FromIterator;

#[derive(Debug)]
enum Short {
    Stopped,
    Completed,
}

impl<T> FromIterator<Option<T>> for Short {
    fn from_iter<I: IntoIterator<Item = Option<T>>>(iter: I) -> Self {
        for x in iter {
            if let None = x { return Short::Stopped }
        }
        Short::Completed
    }
}
fn weird_function(iter: impl IntoIterator<Item=u32>) -> Short {
    iter.into_iter().map(|x| x * 2).map(|x| {
        if x == 8 { return None } // short circuit!
        println!("{}", x);
        Some(x) // keep going!
    }).collect()
}

fn main() {
    println!("{:?}", weird_function(1..10));
}