Return 依赖于函数内分配的数据的惰性迭代器
Return lazy iterator that depends on data allocated within the function
我是 Rust 新手,正在阅读 The Rust Programming Language,并在 Error Handling 部分 there is a "case study" 描述了一个使用 csv
和 rustc-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)?
我是 Rust 新手,正在阅读 The Rust Programming Language,并在 Error Handling 部分 there is a "case study" 描述了一个使用 csv
和 rustc-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 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)?