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

Eli*_*ing 7 iterator allocation heap-memory lifetime rust

我是Rust的新手并阅读了Rust编程语言,在错误处理部分有一个"案例研究",描述了一个程序,使用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()
}
Run Code Online (Sandbox Code Playgroud)

DataRow类型仅仅是一个记录,

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

现在,作为可怕的"向读者练习",作者提出了修改此函数以返回迭代器而不是向量(消除调用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))
}
Run Code Online (Sandbox Code Playgroud)

我返回一个类型的特征对象,Box<Iterator<Item=DataRow> + 'a>以便不必暴露内部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)
}
Run Code Online (Sandbox Code Playgroud)

这个问题似乎与同一问题有关,但答案的作者通过提出有关数据来解决static这个问题,我认为这不是这个问题的替代方案.

我正在使用Rust 1.10.0,这是Arch Linux软件包的当前稳定版本rust.

She*_*ter 5

CSV 1.0

正如我在旧版 crate 的答案中提到的那样,解决这个问题的最好方法是让 CSV crate 拥有一个拥有迭代器,现在它这样做了: 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)
}
Run Code Online (Sandbox Code Playgroud)

1.0 版之前

转换原始函数的最直接路径是简单地包装迭代器。但是,直接这样做会导致问题,因为您无法返回引用自身的对象以及decode引用Reader. 如果你能克服这一点,你就不能让迭代器返回对自身的引用

一种解决方案是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()
    }
}
Run Code Online (Sandbox Code Playgroud)

这可能会产生与之相关的开销,具体取决于decode. 此外,这可能会“倒带”回到输入的开头——如果你用 aVec代替 a csv::Reader,你会看到这个。但是,它恰好在这种情况下起作用。

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

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

也可以看看:


归档时间:

查看次数:

342 次

最近记录:

9 年,6 月 前