我是Rust的新手并阅读 The Rust Programming Language ,并在错误处理部分there is a "case study"中描述了从CSV文件中读取数据的程序使用csv
和rustc-serialize
库(使用getopts
进行参数解析)。
作者编写了一个函数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>
}
现在,作为可怕的“向读者练习”,作者提出了修改此函数以返回迭代器而不是向量(消除对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
闭包来解决的问题。但是当我没有返回一个函数时,我不知道如何做类似的事情。我已经尝试定义一个包含所需数据的自定义迭代器类型,但我无法使它工作,并且它变得更加丑陋和更加做作(不要过多地使用这些代码,我只是将它包含在内显示我尝试的大致方向):
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
中的当前稳定版本。
答案 0 :(得分:3)
转换原始函数的最直接路径是wrap the iterator。但是,直接这样做会导致问题,因为you cannot return an object that refers to itself而decode
的结果会引用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别名迭代器。我更喜欢这个,因为代码的结构反映了对象的生命周期。
我有点惊讶的是csv::Reader
没有IntoIterator
的实现,这也可以解决问题,因为没有任何引用。