首页 文章

返回依赖于函数内分配的数据的延迟迭代器

提问于
浏览
7

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

作者编写了一个函数 search ,它使用 csv::Reader 对象逐步执行csv文件的行,并将'city'字段与指定值匹配的条目收集到向量中并返回它 . 我采取了与作者略有不同的方法,但这不应该影响我的问题 . 我的(工作)函数看起来像这样:

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",作者提出修改此函数以返回迭代器而不是向量(消除对 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> 类型的特征对象,以便不必公开内部 Filter 类型,并且引入生命周期 'a 只是为了避免必须创建 city 的本地克隆 . 但这无法编译,因为 reader 的寿命不够长;它被分配在堆栈上,因此在函数返回时被释放 .

我想这意味着 reader 必须从一开始就在堆上分配(即盒装),或者在函数结束之前以某种方式从堆栈中移出 . 如果我返回一个闭包,这正是通过使它成为 move 闭包来解决的问题 . 但我没有't know how to do something similar when I'我没有返回一个功能 . 我开始工作了,它一直变得更加丑陋和更加狡猾(仅仅包括它以显示我尝试的大方向):

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 的当前稳定版本 .

1 回答

  • 3

    转换原始函数的最直接路径是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 的实现 . 另外,这可能会"rewind"回到输入的开头 - 如果你替换 Vec 而不是 csv::Reader ,你会看到这一点 . 但是,它恰好在这种情况下起作用 .

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

    IntoIteratorIntoIteratorIntoIterator的实现,这也将解决问题,因为没有任何引用 .

相关问题