Return 依赖于函数内分配的数据的惰性迭代器

Return lazy iterator that depends on data allocated within the function

我是 Rust 新手,正在阅读 The Rust Programming Language,并在 Error Handling 部分 there is a "case study" 描述了一个使用 csvrustc-serialize 库(使用 getopts 进行参数解析)从 CSV 文件读取数据的程序。

作者编写了一个函数 search,它使用 csv::Reader 对象遍历 csv 文件的行,并将那些 'city' 字段与指定值匹配的条目收集到向量中return就这样了。我采取了与作者略有不同的方法,但这不应该影响我的问题。我的(工作)函数如下所示:

extern crate csv;
extern crate rustc_serialize;

use std::path::Path;
use std::fs::File;

fn search<P>(data_path: P, city: &str) -> Vec<DataRow>
    where P: AsRef<Path>
{
    let file = File::open(data_path).expect("Opening file failed!");
    let mut reader = csv::Reader::from_reader(file).has_headers(true);

    reader.decode()
          .map(|row| row.expect("Failed decoding row"))
          .filter(|row: &DataRow| row.city == city)
          .collect()
}

其中 DataRow 类型只是一条记录,

#[derive(Debug, RustcDecodable)]
struct DataRow {
    country: String,
    city: String,
    accent_city: String,
    region: String,
    population: Option<u64>,
    latitude: Option<f64>,
    longitude: Option<f64>
}

现在,作为可怕的 "exercise to the reader",作者提出了将此函数修改为 return 迭代器而不是向量的问题(消除了对 collect 的调用)。我的问题是:如何才能做到这一点,最简洁、最惯用的方法是什么?


我认为获得正确类型签名的简单尝试是

fn search_iter<'a,P>(data_path: P, city: &'a str)
    -> Box<Iterator<Item=DataRow> + 'a>
    where P: AsRef<Path>
{
    let file = File::open(data_path).expect("Opening file failed!");
    let mut reader = csv::Reader::from_reader(file).has_headers(true);

    Box::new(reader.decode()
                   .map(|row| row.expect("Failed decoding row"))
                   .filter(|row: &DataRow| row.city == city))
}

我return一个Box<Iterator<Item=DataRow> + 'a>类型的特征对象,这样就不必暴露内部Filter类型,而引入生命周期'a只是为了避免必须制作 city 的本地克隆。但这无法编译,因为 reader 的寿命不够长;它分配在堆栈上,因此在函数 returns.

时被释放

我想这意味着 reader 必须从一开始就在堆上分配(即装箱),或者在函数结束之前以某种方式移出堆栈。如果我要 return 闭包,这正是通过将其设为 move 闭包可以解决的问题。但是当我没有 return 函数时,我不知道如何做类似的事情。我已经尝试定义一个包含所需数据的自定义迭代器类型,但我无法让它工作,而且它变得越来越丑陋和做作(不要过多地使用这段代码,我只是将它包含在显示我尝试的大体方向):

fn search_iter<'a,P>(data_path: P, city: &'a str)
    -> Box<Iterator<Item=DataRow> + 'a>
    where P: AsRef<Path>
{
    struct ResultIter<'a> {
        reader: csv::Reader<File>,
        wrapped_iterator: Option<Box<Iterator<Item=DataRow> + 'a>>
    }

    impl<'a> Iterator for ResultIter<'a> {
        type Item = DataRow;

        fn next(&mut self) -> Option<DataRow>
        { self.wrapped_iterator.unwrap().next() }
    }

    let file = File::open(data_path).expect("Opening file failed!");

    // Incrementally initialise
    let mut result_iter = ResultIter {
        reader: csv::Reader::from_reader(file).has_headers(true),
        wrapped_iterator: None // Uninitialised
    };
    result_iter.wrapped_iterator =
        Some(Box::new(result_iter.reader
                                 .decode()
                                 .map(|row| row.expect("Failed decoding row"))
                                 .filter(|&row: &DataRow| row.city == city)));

    Box::new(result_iter)
}

似乎关注同样的问题,但是答案的作者通过制作相关数据 static 来解决它,我认为这不是这个问题的替代方案。

我正在使用 Rust 1.10.0,Arch Linux 包 rust 中的当前稳定版本。

CSV 1.0

正如我在旧版本箱子的回答中提到的,解决这个问题的最好方法是让 CSV 箱子有一个拥有的迭代器,它现在这样做了:DeserializeRecordsIntoIter

use csv::ReaderBuilder; // 1.1.1
use serde::Deserialize; // 1.0.104
use std::{fs::File, path::Path};

#[derive(Debug, Deserialize)]
struct DataRow {
    country: String,
    city: String,
    accent_city: String,
    region: String,
    population: Option<u64>,
    latitude: Option<f64>,
    longitude: Option<f64>,
}

fn search_iter(data_path: impl AsRef<Path>, city: &str) -> impl Iterator<Item = DataRow> + '_ {
    let file = File::open(data_path).expect("Opening file failed");

    ReaderBuilder::new()
        .has_headers(true)
        .from_reader(file)
        .into_deserialize::<DataRow>()
        .map(|row| row.expect("Failed decoding row"))
        .filter(move |row| row.city == city)
}

1.0 之前的版本

转换原始函数的最直接路径是 wrap the iterator. However, doing so directly will lead to problems because and the result of decode refers to the Reader. If you could surmount that, you cannot have an iterator return references to itself

一个解决方案是为每次调用新迭代器简单地重新创建 DecodedRecords 迭代器:

fn search_iter<'a, P>(data_path: P, city: &'a str) -> MyIter<'a>
where
    P: AsRef<Path>,
{
    let file = File::open(data_path).expect("Opening file failed!");

    MyIter {
        reader: csv::Reader::from_reader(file).has_headers(true),
        city: city,
    }
}

struct MyIter<'a> {
    reader: csv::Reader<File>,
    city: &'a str,
}

impl<'a> Iterator for MyIter<'a> {
    type Item = DataRow;

    fn next(&mut self) -> Option<Self::Item> {
        let city = self.city;

        self.reader
            .decode()
            .map(|row| row.expect("Failed decoding row"))
            .filter(|row: &DataRow| row.city == city)
            .next()
    }
}

这可能会产生相关开销,具体取决于 decode 的实施。此外,这可能 "rewind" 回到输入的开头——如果你用 Vec 代替 csv::Reader,你会看到这个。但是,它恰好适用于这种情况。

除此之外,我通常会打开文件并在函数外部创建 csv::Reader 并传入 DecodedRecords 迭代器并对其进行转换,返回一个 newtype / box / type 别名底层迭代器。我更喜欢这个,因为你的代码结构反映了对象的生命周期。

我有点惊讶 csv::Reader 没有 IntoIterator 的实现,这也可以解决问题,因为没有任何引用。

另请参阅:

  • How can I store a Chars iterator in the same struct as the String it is iterating on?
  • What is the correct way to return an Iterator (or any other trait)?