我是Rust的新手,正在阅读Rust编程语言,并且在“错误处理”部分there is a "case study"中描述了一个程序,该程序使用csv
和rustc-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 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别名。我更喜欢这样做,因为您的代码结构反射(reflect)了对象的生命周期。对于
IntoIterator
没有 csv::Reader
的实现,我感到有些惊讶,这也将解决该问题,因为将没有任何引用。也可以看看:
关于iterator - 返回依赖于函数内分配数据的惰性迭代器,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/38797960/