2016-08-05 44 views
7

Jestem nowy w Rust i czytanie Rust Programming Language, aw Error Handling sekcja there is a "case study" opisujący program do odczytu danych z pliku CSV, używając csv i rustc-serialize bibliotek (używając getopts do analizy argumentów).Powrót leniwe iterator, który zależy od danych przydzielonych w ramach funkcji

Autor pisze funkcję search, która przechodzi przez wiersze pliku csv za pomocą obiektu csv::Reader i zbiera te wpisy, których pole "city" jest zgodne z określoną wartością do wektora i zwraca je. Podjąłem nieco inne podejście niż autor, ale nie powinno to wpłynąć na moje pytanie. My (roboczy) funkcja wygląda tak:

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() 
} 

gdzie typ DataRow jest tylko zapis,

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

Teraz, autor stawia, jak strasznych „ćwiczenie dla czytelnika”, problem modyfikowania tej funkcji, aby zwrócić iterator zamiast wektora (eliminując wywołanie do collect). Moje pytanie brzmi: jak to w ogóle można zrobić i jakie są najbardziej zwięzłe i idiomatyczne sposoby robienia tego?


Prosta próba że myślę dostaje podpis typu prawo jest

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)) 
} 

wrócę obiekt cecha typu Box<Iterator<Item=DataRow> + 'a> tak aby nie trzeba wystawiać wewnętrzną Filter rodzaj, a gdzie życie 'a została wprowadzona tylko po to, aby uniknąć konieczności tworzenia lokalnego klona o numerze city. Ale to się nie kompiluje, ponieważ reader nie żyje wystarczająco długo; jest przydzielany na stosie, a więc jest zwalniany, gdy funkcja zwraca.

Domyślam się, że oznacza to, że reader musi zostać przydzielone na stercie (tj. W pudełku) od początku, lub w jakiś sposób przeniesione ze stosu przed zakończeniem funkcji. Jeśli zwróciłem zamknięcie, jest to dokładnie problem, który zostałby rozwiązany przez zamknięcie go na move. Ale nie wiem, jak zrobić coś podobnego, gdy nie otrzymuję funkcji. Próbowałem zdefiniować niestandardowy typ iteratora zawierający potrzebne dane, ale nie mogłem go uruchomić, a on ciągle robił się coraz brzydszy i bardziej wymyślny (nie rób zbyt dużo tego kodu, włączam go tylko do pokazać ogólny kierunek moich prób):

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 wydaje się dotyczyć tego samego problemu, ale autor odpowiedzi rozwiązuje go poprzez odnośnych danych static, które nie sądzę, jest alternatywą dla tego pytanie.

Używam Rust 1.10.0, aktualna stabilna wersja z pakietu Arch Linux rust.

+4

Chciałbym podziękować za zadanie zadawanego pytania. Wielu odwiedzających często nie okazuje zbyt wielu przygotowań, a tym bardziej nie pyta po raz pierwszy. Sława! – Shepmaster

+1

@Shepmaster Dzięki, starałem się jak najlepiej napisać dobre pierwsze pytanie, i wydaje się, że mam na to dobrze wykwalifikowaną odpowiedź! Mimo to, dziękuję za twoje korekty stylistyczne. –

Odpowiedz

3

Najprostszą drogą do przekonwertowania oryginalnej funkcji będzie po prostu wrap the iterator. Jednak wykonanie tej czynności bezpośrednio spowoduje problemy, ponieważ you cannot return an object that refers to itself i wynik decode odnoszą się do Reader. Jeśli mógłbyś to przezwyciężyć, to masz cannot have an iterator return references to itself.

Jednym z rozwiązań jest po prostu odtworzyć DecodedRecords iterator dla każdego wywołania do nowego iteratora:

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() 
    } 
} 

Może to mieć narzutu związanego z nim, w zależności od wykonania decode. Dodatkowo może to "cofnąć" z powrotem na początek wejścia - jeśli zastąpisz Vec zamiast csv::Reader, zobaczysz to. Jednak zdarza się, że działa w tym przypadku.

Poza tym normalnie otwieram plik i utworzę csv::Reader poza funkcją i przekazuję go w iteratorze DecodedRecords i przekształcam go, zwracając alias typu/typu/typu wokół leżącego poniżej iteratora. Wolę to, ponieważ struktura twojego kodu odzwierciedla okresy życia obiektów.

Jestem trochę zaskoczony, że nie ma implementacji IntoIterator dla csv::Reader, co również rozwiązałoby problem, ponieważ nie byłoby żadnych odniesień.

+0

Dziękuję za odpowiedź! Odtworzenie iteratora wydaje mi się nieco obrzydliwe, ale z pewnością działa i przypuszczam, że byłoby lepiej, gdyby zaimplementowano "IntoIterator". Wygląda na to, że mieliśmy szczęście, że 'Reader' nie przewinął się, jak mówisz (uznałem to za sprzeczne z intuicją, przewijanie jest zwykłym zachowaniem iteratorów w moim doświadczeniu); w przeciwnym razie program musiałby zostać poddany restrukturyzacji. Jestem przyzwyczajony do pisania funkcji mniej więcej jako czarnych skrzynek, które odzwierciedlają mój mentalny model kroków rozwiązywania problemów, ale może to nie jest rozsądna "abstrakcja zero-kosztów" w Rust. –