2016-08-05 67 views
7

Ich bin neu in Rust in der Funktion zugeordnet auf Daten abhängig und Handling und im Fehler Die Rust Programming Language Lesen Abschnitt there is a "case study" ein Programm beschreiben, Daten aus einer CSV-Datei zu lesen, die csv mit und rustc-serialize Bibliotheken (unter Verwendung von getopts zum Parsen von Argumenten).Rückkehr faul Iterator,

Der Autor schreibt eine Funktion search, die durch die Zeilen der csv-Datei mit einem csv::Reader-Objekt schreitet und diejenigen Einträge sammelt, deren "Stadt" -Feld einen bestimmten Wert in einen Vektor einfügt und zurückgibt. Ich habe einen etwas anderen Ansatz gewählt als der Autor, aber das sollte meine Frage nicht beeinflussen. Mein (Arbeits-) Funktion sieht wie folgt aus:

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

wo der DataRow Typ ist nur ein Datensatz,

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

Nun, der Autor stellt, als die gefürchtete „Übung für den Leser“, das Problem Ändern dieser Funktion, um einen Iterator anstelle eines Vektors zurückzugeben (Eliminieren des Aufrufs an collect). Meine Frage ist: Wie kann das überhaupt gemacht werden und was sind die prägnantesten und idiomatischsten Wege, dies zu tun?


Ein einfacher Versuch, die ich denke, wird die Art Signatur richtig ist

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

ich ein Merkmal Objekt vom Typ zurückgeben Box<Iterator<Item=DataRow> + 'a>, um nicht den internen Filter Typen zu haben zu belichten, und wo die Lebensdauer 'a wird nur eingeführt, um zu vermeiden, dass ein lokaler Klon von city erstellt werden muss. Aber das kompiliert nicht, weil reader nicht lange genug lebt; es ist auf dem Stapel zugeordnet und wird daher freigegeben, wenn die Funktion zurückkehrt.

Ich denke, das bedeutet, dass reader auf den Haufen (d. H. Boxed) von Anfang an zugewiesen werden muss, oder irgendwie aus dem Stapel verschoben, bevor die Funktion endet. Wenn ich eine Schließung zurücksende, ist dies genau das Problem, das gelöst werden würde, indem man es zu einer move Schließung macht. Aber ich weiß nicht, wie ich etwas ähnliches machen soll, wenn ich keine Funktion zurückgebe. Ich habe versucht, einen benutzerdefinierten Iteratortyp zu definieren, der die benötigten Daten enthält, aber ich konnte es nicht zum Laufen bringen, und es wurde immer hässlicher und ausgeklügelter (machen Sie nicht zu viel von diesem Code, ich schließe ihn nur ein zeigen die allgemeine Richtung meiner Versuche):

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 scheint das gleiche Problem zu befassen, sondern der Verfasser der Antwort löst sie, indem sie die betreffenden Daten machen static, die ich nicht glaube, ist eine Alternative für diese Frage.

Ich verwende Rust 1.10.0, die aktuelle stabile Version von Arch Linux-Paket rust.

+4

Ich möchte Ihnen danken, dass Sie eine gut konstruierte Frage gestellt haben. Viele häufige Besucher zeigen nicht so viel Vorbereitung, geschweige denn Erstsucher. Kudos! – Shepmaster

+1

@Shempmaster Danke, ich habe mein Bestes gegeben, um eine gute erste Frage zu schreiben, und es scheint, ich habe eine gut qualifizierte Antwort dafür bekommen! Trotzdem, danke für deine stilistischen Korrekturen. –

Antwort

3

Der direkteste Weg die ursprüngliche Funktion zu konvertieren einfach wrap the iterator wäre. Allerdings werden die Probleme so direkt zu tun führen, weil you cannot return an object that refers to itself und das Ergebnis der decode zum Reader bezieht. Wenn Sie das überwinden könnten, Sie cannot have an iterator return references to itself.

Eine Lösung ist, einfach neu zu erstellen, die DecodedRecords Iterator für jeden Anruf zu Ihrem neuen Iterator:

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

Dieser Overhead mit sich, abhängig von der Implementierung von decode zugeordnet haben könnte. Darüber hinaus könnte dies "zurückspulen" zurück zum Anfang der Eingabe - wenn Sie eine Vec statt einer csv::Reader ersetzen, würden Sie dies sehen. Es funktioniert jedoch in diesem Fall.

Darüber hinaus würde ich normalerweise öffnen Sie die Datei und erstellen Sie die csv::Reader außerhalb der Funktion und übergeben Sie die DecodedRecords Iterator und transformieren Sie es, einen neuen Typ/Box/Typ Alias ​​um den zugrunde liegenden Iterator. Ich bevorzuge das, weil die Struktur Ihres Codes die Lebensdauer der Objekte widerspiegelt.

Ich bin ein wenig überrascht, dass es keine Implementierung von IntoIterator für csv::Reader gibt, die das Problem auch lösen würde, weil es keine Referenzen geben würde.

+0

Vielen Dank für Ihre Antwort! Das Nachbilden des Iterators erscheint mir etwas abscheulich, aber es funktioniert sicherlich und ich denke, es wäre schöner gewesen, wenn "IntoIterator" implementiert worden wäre. Es sieht so aus, als hätten wir Glück gehabt, dass der "Reader" nicht zurückgespult hat, wie du sagst (ich fand diesen Gegensinn; Rückspulen ist das übliche Verhalten von Iteratoren in meiner Erfahrung); andernfalls hätte das Programm möglicherweise neu strukturiert werden müssen. Ich bin es gewohnt, Funktionen mehr oder weniger als Black Boxes zu schreiben, die mein mentales Modell der Problemlösungsschritte widerspiegeln, aber vielleicht ist das keine vernünftige "Null-Kosten-Abstraktion" in Rust. –