为什么在使用 `flat_map` 时需要收集到一个向量中?

Why do I need to collect into a vector when using `flat_map`?

我正在 Project Euler 96 to teach myself Rust. I've written this code to read in the file and convert it into a vector of integers (Playground).

let file = File::open(&args[1]).expect("Sudoku file not found");
let reader = BufReader::new(file);

let x = reader
    .lines()
    .map(|x| x.unwrap())
    .filter(|x| !x.starts_with("Grid"))
    .flat_map(|s| s.chars().collect::<Vec<_>>())  // <-- collect here!
    .map(|x| x.to_digit(10).unwrap())
    .collect::<Vec<_>>();

这一切都很好,但我很困惑为什么我必须在我的 flat_map 中收集到一个矢量(我假设创建不需要的矢量会立即被销毁是低效的)。如果我不收集,它就不会编译:

error[E0515]: cannot return value referencing function parameter `s`
  --> src/main.rs:13:23
   |
13 |         .flat_map(|s| s.chars())
   |                       -^^^^^^^^
   |                       |
   |                       returns a value referencing data owned by the current function
   |                       `s` is borrowed here

来自 the docs 的示例显示几乎相同的代码,但不需要收集:

let words = ["alpha", "beta", "gamma"];

// chars() returns an iterator
let merged: String = words.iter()
                          .flat_map(|s| s.chars())
                          .collect();
assert_eq!(merged, "alphabetagamma");

为什么我的代码不一样?

迭代器 reader.lines().map(|x| x.unwrap()) 迭代 String 项,即按值。因此,在 .flat_map(|s| ...) 中,变量 s 的类型为 String(即拥有,而非借用)。换句话说:字符串现在是一个局部变量,存在于函数中。这是一个简单的规则,您不能 return 引用局部变量(参见 this Q&A)。但这正是 s.chars() 所做的,即使它有点隐藏。

正在看at str::chars:

pub fn chars(&self) -> Chars<'_>

可以看出字符串是借用的。 returned Chars 对象包含对原始字符串的引用。这就是为什么我们不能从闭包中 return s.chars()

So why is it different in my code?

在文档的示例中,迭代器 words.iter() 实际上迭代 &&'static str 类型的项目。调用 s.chars() 也会 return 借用一些字符串的 Chars 对象,但是该字符串的生命周期是 'static (永远存在),所以 [=59= 没有问题]ing Chars 从闭包中。

解决方案?

如果标准库有一个使用 StringOwnedChars 迭代器,那就太好了,就像 Chars 一样工作,并在迭代器被删除后删除字符串。在这种情况下,调用 s.owned_chars() 没问题,因为 returned 对象不引用本地 s,但拥有它。但是:标准库中不存在这样的自有迭代器!

I'm assuming creating unneeded vectors which will be immediately destroyed is inefficient

是的,这在某种程度上是正确的。但是您可能错过了 reader.lines() 迭代器还会创建 String 类型的临时对象。那些或多或少也立即被摧毁了!因此,即使 flat_map 中没有 collect,您也会有一堆不必要的分配。请注意,有时这没关系。在这种情况下,我猜想与您必须实现的实际算法相比,输入解析速度非常快。那么...只是collect?在这种情况下可能没问题。

如果你想要高性能的输入解析,我认为你将无法避免标准循环,特别是为了避免不必要的 String 分配。 (Playground)

let mut line = String::new();
let mut input = Vec::new();
loop {
    line.clear(); // clear contents, but keep memory buffer

    // TODO: handle IO error properly
    let bytes_read = reader.read_line(&mut line).expect("IO error"); 
    if bytes_read == 0 {
        break;
    }

    if line.starts_with("Grid") {
        continue;
    }

    // TODO: handle invalid input error
    input.extend(line.trim().chars().map(|c| c.to_digit(10).unwrap()));
}

除了另一个答案,请注意自有迭代器很容易编写:

struct OwnedChars {
    s: String,
    i: usize,
}

impl Iterator for OwnedChars {
    type Item = char;

    fn next(&mut self) -> Option<Self::Item> {
        let c = self.s[self.i..].chars().next()?;

        self.i += c.len_utf8();

        Some(c)
    }
}

fn into_iter(string: String) -> OwnedChars {
    OwnedChars {
        s: string,
        i: 0,
    }
}

fn main() {
    let owned_iter = into_iter("Zażółć gęślą jaźń".into());

    for c in owned_iter {
        println!("{}", c);
    }
}

然后,你不需要收集:

fn main() {
    use std::io::prelude::*;

    let file = std::fs::File::open(std::env::args().nth(1).unwrap()).expect("Sudoku file not found");
    let reader = std::io::BufReader::new(file);

    let x = reader
        .lines()
        .map(|x| x.unwrap())
        .filter(|x| !x.starts_with("Grid"))
        .flat_map(into_iter)
        .map(|x| x.to_digit(10).unwrap())
        .collect::<Vec<_>>();
}