2016-08-05 11 views
7

Я новичок в Русте и чтение Ржавчина Язык программирования, и в Error Handling раздел there is a "case study" с описанием программы для чтения данных из файла CSV с помощью csv и rustc-serialize (с использованием getopts для анализа аргументов).Возврат ленивый итератор, который зависит от данных, выделенных в рамках функции

Автор пишет функцию search, которая выполняет шаги в строках csv-файла с использованием объекта csv::Reader и собирает те записи, чье поле «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.

+4

Я хотел бы поблагодарить вас за то, что вы задали хороший вопрос. Многие частые посетители не показывают столько подготовки, а тем более не в первый раз. Престижность! – Shepmaster

+1

@Shepmaster Спасибо, я изо всех сил старался написать хороший первый вопрос, и, похоже, я получил для него квалифицированный ответ! Тем не менее, спасибо за ваши стилистические исправления. –

ответ

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 вокруг базового итератора. Я предпочитаю это, потому что структура вашего кода отражает время жизни объектов.

Я немного удивлен, что не существует IntoIterator для csv::Reader, что также решит проблему, потому что ссылок не будет.

+0

Спасибо за ваш ответ! Воссоздание итератора поражает меня как-то отвратительно, но это, безусловно, работает, и я полагаю, было бы лучше, если бы «IntoIterator» был реализован. Похоже, нам повезло, что «Читатель» не перемотался, как вы говорите (я нашел эту контратакую, перемотка - это обычное поведение итераторов в моем опыте); в противном случае программу, возможно, пришлось бы реструктурировать. Я привык писать функции более или менее как черные ящики, которые отражают мою ментальную модель шагов решения проблем, но, возможно, это не разумная «нулевая стоимость абстракции» в Rust. –