为什么返回迭代器是个难题
\n\n你是否曾经用Rust编写过一个函数,它需要返回一个经过迭代器链处理过的集合?如果是的话,你很可能遇到过这样的问题:“该返回什么类型?” 似乎很自然地会写成 fn get_data() -> Iterator,但编译器会立刻报错:“the trait `Iterator` cannot be made into an object”。
这个问题非常普遍,以至于在Stack Overflow上,“What is the correct way to return an Iterator (or any other trait)?”这个问题获得了数千次浏览。这并不奇怪——正确的选择不仅影响代码的便利性,还影响性能。在本文中,我们将探讨在Rust中返回迭代器的所有方法、它们的优缺点,以及如何避免在不经意间损失性能。
\n\n本文是为 pythonlib.ru 编写的——这是一个教育网站,我们乐于深入探究流行语言的底层机制。Rust凭借其零成本抽象,为我们提供了编写既优雅又快速的代码的独特机会。但前提是我们知道如何正确使用它的工具。
\n\n问题:trait没有固定大小
\n\n在Rust中,trait是接口,但与Java或Go不同,它们不是具有已知大小的类型。编译器需要知道返回值占用多少内存,以便在栈上分配空间。而迭代器的具体实现(例如 std::iter::Map 或 std::iter::Filter)的大小可能不同。
因此,不能直接返回 Iterator。我们必须选择以下模式之一:
- \n
- 静态分发 ——
impl Iterator(返回具体类型) \n - 动态分发 ——
Box<dyn Iterator>(类型被擦除) \n - 枚举分发 —— 为所有可能的迭代器创建自己的枚举 \n
- 泛型 —— 函数接受并返回泛型类型 \n
每种方法都有其代价。让我们通过实际示例来逐一分析。
\n\n方法1:impl Iterator —— 黄金标准
\n\n最简单且性能最高的方法是使用返回类型中的 impl Trait。这是一种语法糖,它告诉编译器:“我将返回某个实现了该trait的具体类型,但由你来推断它”。
fn get_numbers() -> impl Iterator<Item = i32> {\n (0..10).map(|x| x * 2)\n}\n\nfn main() {\n for num in get_numbers() {\n println!("{}", num);\n }\n}\n\n优点:
\n- \n
- 最高性能 —— 静态分发,没有虚函数调用 \n
- 编译器可以内联代码并应用优化 \n
- 生成的二进制文件体积最小 \n
缺点:
\n- \n
- 返回类型必须是单一的 —— 不能在某些情况下返回
Map,在另一些情况下返回Filter\n - 类型可能非常复杂(例如
Map<Filter<...>>),导致编译器错误信息难以阅读 \n - 不能存储在结构体或向量中 —— 只能在函数中使用 \n
这种方法在90%的情况下是理想的。如果你的函数总是返回相同的迭代器链,请毫不犹豫地使用 impl Iterator。
方法2:Box<dyn Iterator> —— 以性能换取灵活性
\n\n当你需要根据条件(例如解析命令行参数时)返回不同类型的迭代器时,动态分发 通过 Box<dyn Iterator> 提供了帮助。
fn get_data(use_filter: bool) -> Box<dyn Iterator<Item = i32>> {\n let data = vec![1, 2, 3, 4, 5];\n if use_filter {\n