我是Rust的新手,正在阅读Rust编程语言,并且在“错误处理”部分there is a "case study"中描述了一个程序,该程序使用csvrustc-serialize库(使用getopts进行参数解析)从CSV文件中读取数据。

作者编写了一个search函数,该函数使用csv::Reader对象逐步浏览csv文件的行,并将“城市”字段与指定值匹配的条目收集到向量中并返回。我采用的方法与作者略有不同,但这不应影响我的问题。我的(工作)函数如下所示:

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>
}

现在,作者提出了一个令人恐惧的“读者练习”的问题,即修改此函数以返回迭代器而不是向量(消除对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))
}

我返回了Box<Iterator<Item=DataRow> + 'a>类型的trait对象,以便不必公开内部Filter类型,并且在其中引入了生命周期'a只是为了避免必须本地创建city。但这无法编译,因为reader的生存时间不够长。它分配在堆栈上,因此在函数返回时被释放。

我想这意味着reader必须从头开始分配在堆上(即盒装),或者以某种方式在函数结束之前从堆栈中移出。如果我要返回一个闭包,这正是将其设置为move闭包可以解决的问题。但是当我不返回函数时,我不知道如何做类似的事情。我曾尝试定义一个包含所需数据的自定义迭代器类型,但我无法使其正常工作,并且它变得越来越丑陋且更加虚构(不要花太多代码,我只将其包括在内)中。显示我尝试的大致方向):
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)
}

This question似乎也涉及相同的问题,但是答案的作者通过将相关数据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。但是,直接这样做会导致问题,因为you cannot return an object that refers to itselfdecode的结果引用了Reader。如果可以克服的话,请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的实现,这可能会有相关的开销。此外,这可能会“倒回”到输入的开头-如果您用Vec而不是csv::Reader替换,则会看到此信息。但是,它恰好在这种情况下起作用。

除此之外,我通常会打开文件并在函数外部创建csv::Reader,并传递DecodedRecords迭代器并对其进行转换,从而在基础迭代器周围返回一个newtype/box/type别名。我更喜欢这样做,因为您的代码结构反射(reflect)了对象的生命周期。

对于IntoIterator没有 csv::Reader 的实现,我感到有些惊讶,这也将解决该问题,因为将没有任何引用。

也可以看看:
  • How can I store a Chars iterator in the same struct as the String it is iterating on?
  • Is there an owned version of String::chars?
  • What is the correct way to return an Iterator (or any other trait)?
  • 关于iterator - 返回依赖于函数内分配数据的惰性迭代器,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/38797960/

    10-09 16:29
    查看更多