Objektidentität vom Ausrüstungstyp trennen

RustBeginner
Jetzt üben

Einführung

Ein Team im Außeneinsatz benötigt zwei funktionierende Taschenlampen. Zwei Scan-Zeilen können zwei verschiedene Lampen, eine zweimal gescannte Lampe oder eine intakte und eine beschädigte Lampe beschreiben. Dein Programm muss diese Unterschiede erhalten, bevor es beurteilen kann, ob die Ausrüstung vollständig ist.

In diesem Lab implementierst du in Rust einen Zustandsparser und eine Objektsammlung. Du behältst verschiedene physische Objekte, ignorierst identische Scan-Wiederholungen und lehnst widersprüchliche Datensätze ab, ohne die Quelldateien zu ändern. Das nächste Lab verwendet diese Identitäten, um Fehlmengen oder Überschüsse je Ausrüstungstyp zu erklären. Die Datensätze sind synthetisch; es werden keine realen Ausrüstungsgegenstände inspiziert.

Du solltest Rust-Strukturen und -Enumerationen, HashMap, Result, das Lesen von Textdateien und grundlegende for-Schleifen bereits kennen. Der vorbereitete Parser, die Befehlsoberfläche und die Ausgabeformatierung helfen dir, dich auf Identitätsentscheidungen zu konzentrieren. Der Autor schätzt 45 Minuten einschließlich Lesen und üblicher Korrekturen; die Dauer wurde nicht mit Anfängern gemessen.

Zustände ohne Raten einlesen

In diesem Schritt untersuchst du zwei tatsächliche Scan-Dateien, sagst ihre nutzbaren Gegenstände voraus und implementierst die kurze Zustandsentscheidung, mit der die normale Zeilenansicht des Programms funktioniert. Öffne Terminal. Das vorbereitete Projekt liegt unter /home/labex/project/field-kit und verwendet Rust 1.99.1.

Das folgende export ergänzt den Befehls-Suchpfad dieser Shell um das Cargo-Verzeichnis. $PATH behält die bisherigen Verzeichnisse. Dies gilt nur für die aktuelle Terminal-Sitzung; wiederhole es in einer neuen Shell. Es verändert keine Startdateien der Shell.

export PATH=/home/labex/.cargo/bin:$PATH

Wähle mit cd das Projekt; wiederhole es, falls du dein Verzeichnis verlierst. pwd zeigt das aktuelle Verzeichnis, damit du den Ort späterer Dateibearbeitungen bestätigen kannst.

cd /home/labex/project/field-kit
pwd

Erwarte /home/labex/project/field-kit. Korrigiere einen Tippfehler im Pfad mit cd, nicht durch Zurücksetzen der VM. cat zeigt eine Datei an, ohne sie zu verändern. Lies diese zwei tatsächlichen Eingaben:

cat data/two-good.tsv
cat data/one-damaged.tsv

TSV bedeutet durch Tabulatoren getrennte Werte. Jeder Scan hat drei durch echte Tabulatoren getrennte Felder: asset_id, type_id, condition. In der ersten Datei haben die physischen Objekte a und b beide den Zustand good. In der zweiten ist a intakt und b ist damaged. Prüfe das Ergebnis von Hand: Beide Dateien enthalten zwei physische Gegenstände, aber nur die erste beschreibt zwei nutzbare Lampen. Die Wörter good und damaged sind die zwei exakt erlaubten Zustände; beschädigte Ausrüstung ist gültige Eingabe, keine fehlerhafte Zeile.

Das Projekt stellt src/lib.rs mit Strukturen und TSV-Lesern, src/main.rs mit Befehlsoberfläche und Formatierung sowie src/readiness.rs für das spätere Lab bereit. Lasse diese Dateien, Cargo.toml, rust-toolchain.toml und jede Originaldatei unter data/ unverändert. Hier darfst du nur src/condition.rs und im nächsten Schritt src/identity.rs bearbeiten; erhalte ihre öffentlichen Namen und Typen.

Lies das Zustandsmodul:

cat src/condition.rs

Enumeration und Anzeigemethode sind bereits vorhanden. impl gruppiert Methoden für Condition; Self bezeichnet innerhalb des Blocks diese Enumeration. pub macht diese Namen für den Leser zugänglich. derive liefert Kopieren, Debug-Ausgabe und Gleichheit. parse liefert Result<Self, String>: entweder Ok mit einem Zustand oder Err mit einer Fehlererklärung, die ihre Daten besitzt. Der aktuelle Platzhalter lehnt jedes Wort ab und unterscheidet die zwei normalen Dateien deshalb noch nicht.

Lege vor der Bearbeitung einen Wiederherstellungspunkt nur für den Quelltext an. mktemp -d erstellt ein neues Verzeichnis, indem es die sechs X-Zeichen durch ein eindeutiges Suffix ersetzt; es verwendet keine ältere Sicherung erneut. $() speichert den ausgegebenen Pfad in einer Sitzungsvariablen. && führt cp -p nur nach erfolgreicher Verzeichniserstellung aus; die Kopie erhält die Metadaten des Quelltexts. Anführungszeichen halten den Variablenpfad zusammen. Schlägt eine der Operationen fehl, lies den Fehler und erstelle vor der Bearbeitung eine erfolgreiche Sicherung:

rs03_before_condition=$(mktemp -d .edit-before-condition.XXXXXX) && cp -p src/condition.rs "$rs03_before_condition/condition.rs"

Öffne das vorhandene Modul mit nano; -c zeigt die Cursorposition:

nano -c src/condition.rs

Nanos Schreibweise ^ bedeutet Strg. Drücke Strg+W, gib pub fn parse ein und drücke Enter, um die Funktion zu finden. Lies ihre öffnende und schließende Klammer. Bearbeite mit Pfeiltasten und Rücktaste/Entf nur den Rumpf dieser Methode und ihre ungenutzte Parameterbindung. Benenne _value in value um und erhalte den Parametertyp. Lasse Enumeration, derive und die Methode name unverändert.

Schreibe einen Ausdruck match value mit einem exakten Zweig für jedes erlaubte Wort und einem Platzhalter für jede andere Zeichenkette. Ein Zweig wie "good" => Ok(Self::Good), zeigt die Syntax. Konstruiere das entsprechende Damaged-Ergebnis selbst. _ trifft auf jeden übrigen Wert zu, der Err zurückgeben muss, statt stillschweigend einen Zustand zu erfinden. format! erzeugt einen String; {value:?} setzt den unerwarteten Text in Anführungszeichen, damit Leerzeichen und Schreibweise in der Diagnose sichtbar bleiben. Verwende es für eine Meldung, die erklärt, dass die Eingabe good oder damaged sein muss. Schließe match und Funktion mit ihren Klammern ab und behalte match als Ergebnis der Methode.

Drücke Strg+O, dann Enter, um src/condition.rs zu bestätigen; Strg+X beendet nano. Gib keine Shell-Befehle ein, solange du noch im Editor bist. Lies die gespeicherte Änderung:

cat src/condition.rs

cargo run kompiliert und startet das vorbereitete Programm. --quiet verbirgt Cargo-Fortschritt, und -- übergibt die folgenden Argumente an das Programm. rows prüft und zeigt jede Zeile der tatsächlichen Datei; Duplikate werden noch nicht entfernt.

cargo run --quiet -- rows data/two-good.tsv
asset_id	type_id	condition
a	light-a	good
b	light-a	good

Die normale Zeilenansicht sollte nun beide IDs mit intaktem Zustand zeigen. Vergleiche sie mit der anderen Quelle:

cargo run --quiet -- rows data/one-damaged.tsv
asset_id	type_id	condition
a	light-a	good
b	light-a	damaged

Dies entspricht deiner Vorhersage von Hand: weiterhin zwei gescannte Gegenstände, nur einer intakt. Du hast eine echte Entscheidung im Aufrufpfad des tatsächlichen Lesers geschrieben, kein separates Demonstrationsprogramm.

Der kontrollierte Eingabevertrag

Lies auch den tatsächlichen Plan:

cat data/plan.tsv

Seine Felder sind type_id, name, required. Sowohl light-a als auch light-b zeigen Flashlight an, sind aber unterschiedliche Typ-IDs. Ein Name ist Anzeigetext, keine Identität. Positiver Bedarf ohne Scans bedeutet eine Fehlmenge; Bedarf null ohne Scans bedeutet Fehlmenge null. Der bereitgestellte Leser wendet diese Regeln an:

  • IDs beginnen mit einem kleinen ASCII-Buchstaben, gefolgt nur von Kleinbuchstaben, Ziffern oder -. Groß- und Kleinschreibung werden unterschieden.
  • Zustände lauten exakt good und damaged; keine andere Schreibweise wird akzeptiert.
  • Namen sind nicht leere, druckbare ASCII-Texte einschließlich Leerzeichen. Mengen bestehen aus Dezimalziffern mit Werten von 0 bis 999; führende Nullen sind erlaubt. Typ-IDs im Plan dürfen sich auch bei identischem Inhalt nicht wiederholen.
  • Die exakte Kopfzeile mit drei Feldern muss zuerst stehen. LF/CRLF, ein fehlender abschließender Zeilenumbruch und spätere vollständig leere Zeilen sind erlaubt. Eine reine Leerraumzeile ist nicht leer. Einzelne Wagenrückläufe sind keine Zeilenenden. Felder werden nicht beschnitten: Leerzeichen in einer ID oder einem Zustand sind Fehler, während Leerzeichen in einem Namen erhalten bleiben.

ASCII umfasst hier einfache englische Buchstaben, Ziffern, Leerzeichen und Satzzeichen; Namen schließen Steuerzeichen aus. Dies ist ein kleines definiertes Format, kein allgemeiner CSV-Leser. Deine Zustandsmethode liefert die Entscheidung zwischen zwei Zuständen innerhalb dieses vorbereiteten Lesers.

Eine Quelltextänderung wiederherstellen

Lies bei einem Rust-Fehler Datei und Zeile von Cargo, öffne diese Datei erneut, suche die betroffene Funktion und repariere die Änderung. Speichere, beende und wiederhole den fehlgeschlagenen Befehl. Verwirf eine versehentliche, noch ungespeicherte Bearbeitung mit Strg+X, dann N bei der Speicherfrage. Dies macht frühere gespeicherte Änderungen nicht rückgängig.

Stelle nur zum Beheben eines gespeicherten Fehlers deinen eigenen Zustandssicherungsstand wieder her, statt das Setup zurückzusetzen. Im selben Terminal bezeichnet die Variable weiterhin die Sicherung. Wiederhole in einem neuen Terminal PATH und cd und liste dann die tatsächlichen Verzeichnisse dieser Phase auf:

ls -d .edit-before-condition.*

-d listet Verzeichnisnamen statt Inhalte; * trifft auf erzeugte Suffixe zu. Führe den folgenden Befehl aus, füge einen exakt ausgegebenen Verzeichnisnamen ein, während die Shell wartet, und drücke Enter. read speichert diese Zeile in der Variablen für diese Sitzung:

read rs03_before_condition

Untersuche die ausgewählte Datei. Sind Pfad oder Phase falsch, wiederhole Auswahl und Prüfung; existiert keine Sicherung, repariere anhand der Referenz, statt ein Suffix zu erraten.

cat "$rs03_before_condition/condition.rs"

Nachdem du deinen beabsichtigten Quelltext vor der Bearbeitung bestätigt hast, ersetzt dieser Befehl gezielt nur die erlaubte Zustandsdatei:

cp -p "$rs03_before_condition/condition.rs" src/condition.rs

Er stellt den unfertigen Parser wieder her. Implementiere die Zustandsentscheidung deshalb erneut und wiederhole beide Zeilenbefehle. Die folgende Referenz dient nach deinem Versuch zum Vergleich einer bestimmten Methode, nicht zum Ersetzen des ganzen Moduls als normale Bearbeitung.

Vollständige Zustandsreferenz für Vergleich und Wiederherstellung
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Condition { Good, Damaged }

impl Condition {
    pub fn parse(value: &str) -> Result<Self, String> {
        match value {
            "good" => Ok(Self::Good),
            "damaged" => Ok(Self::Damaged),
            _ => Err(format!("condition must be good or damaged, got {value:?}")),
        }
    }
    pub fn name(self) -> &'static str {
        match self { Self::Good => "good", Self::Damaged => "damaged" }
    }
}

Physische Objekte anhand ihrer ID sammeln

In diesem Schritt implementierst du die Identitätssammlung, behältst unterschiedliche physische Objekte und zählst identische Wiederholungen einmal. Arbeite weiter in Terminal unter /home/labex/project/field-kit. Lies die größere Quelle:

cat data/scans.tsv

Sage vor dem Programmieren die eindeutigen Gegenstände voraus. Objekt a erscheint zweimal mit demselben Typ und Zustand; a, b und c sind drei verschiedene Lampen des Typs light-a. Objekt d gehört zu light-b, einem anderen Typ mit demselben Anzeigenamen im Plan. e und f gehören zum nicht eingeplanten Typ other; gültige zusätzliche Ausrüstung darf nicht verschwinden. Es sollten sechs physische IDs sein, nicht sieben und nicht eine je Typ oder Name.

Lies das vorhandene Sammlungsgerüst:

cat src/identity.rs

Scan enthält asset_id, type_id, condition und die Diagnoseposition line. Das vorhandene use crate::Scan importiert diese Struktur aus dieser Bibliothek. Die Funktion leiht &[Scan] und darf die Datensätze des Aufrufers deshalb weder verändern noch ihren Besitz übernehmen. Ihr Result liefert einen Vektor eindeutiger Datensätze oder eine hilfreiche Konflikterklärung.

Eine HashMap kann jeder Objekt-ID einen Datensatz zuordnen. Das Einfügen eines vorhandenen Schlüssels ersetzt seinen bisherigen Wert. Schlage ihn deshalb vor dem Einfügen nach. Entscheide getrennt über eine neue ID, eine identische Wiederholung von ID/Typ/Zustand und eine widersprüchliche Wiederholung. Vergleiche nicht den gesamten Scan mit abgeleiteter Gleichheit: Identischer Inhalt tritt naturgemäß in unterschiedlichen Zeilen auf. Vergleiche Typ und Zustand; behalte beide Zeilennummern nur für die Diagnose.

Sichere vor der Bearbeitung den Quelltext dieser Phase:

rs03_before_identity=$(mktemp -d .edit-before-identity.XXXXXX) && cp -p src/identity.rs "$rs03_before_identity/identity.rs"
nano -c src/identity.rs

Dieser Ausgangscode importiert HashMap nicht. Behalte use crate::Scan; und ergänze daneben am Dateianfang den Standardbibliotheksimport:

use std::collections::HashMap;

std ist Rusts Standardbibliothek; crate ist die Bibliothek dieses Projekts. Ein importierter Name ist in diesem Modul verfügbar, nicht in jeder Datei. Eine gleichwertige Implementierung mit geordneter Map oder Sortierung ist ebenfalls erlaubt, mit den tatsächlich benötigten Imports.

Suche mit Strg+W nach pub fn collect_assets und drücke Enter. Benenne _scans in scans um und erhalte seinen Slice-Typ. Implementiere die Entscheidungen der Funktion; lasse andere Definitionen und das fertige Zustandsmodul unverändert.

Ein Ansatz mit Map beginnt mit einer veränderbaren HashMap<String, Scan>. Für jeden geliehenen Scan liefert get(&scan.asset_id) Some mit einem früheren Datensatz oder None. if let Some(previous) betritt den Block nur bei vorhandenem Datensatz. Weicht Typ oder Zustand ab, liefere einen Err mit vollständiger Objekt-ID, aktueller und erster Zeile. format! kann diese Felder zu einer Meldung mit eigenen Daten verbinden. Stimmen die Inhalte überein, überspringt continue das Einfügen der Wiederholung.

Füge für eine neue ID eine geklonte ID und einen geklonten Datensatz ein; .clone() erstellt Kopien mit eigenen Daten aus der geliehenen Eingabe. Verschiebe nach dem Prüfen aller Scans die gespeicherten Datensätze in einen neuen Vec und liefere Ok mit diesem Vektor. Vec::new() erstellt einen leeren Vektor und push hängt ein Element an. In einer Schleife wie for (_, scan) in by_id wird das Paar zerlegt und _ ignoriert den Schlüssel. Die HashMap-Iterationsreihenfolge ist nicht festgelegt; die vorbereitete Formatierung sortiert Objekt-IDs für die Anzeige. Deine interne Reihenfolge muss daher nicht fest sein.

Speichere mit Strg+O, Enter, dann Strg+X. Untersuche vor dem Ausführen deine gespeicherte Funktion:

cat src/identity.rs

Vergleiche zuerst die tatsächlichen Rohzeilen:

cargo run --quiet -- rows data/scans.tsv

Das wiederholte a ist dort weiterhin vorhanden. Rufe nun deine fertige Sammelfunktion auf:

cargo run --quiet -- assets data/scans.tsv
asset_id	type_id	condition
a	light-a	good
b	light-a	good
c	light-a	good
d	light-b	damaged
e	other	good
f	other	damaged

Bestätige alle sechs IDs, Typen und Zustände. Nur die identische Wiederholung verschwand aus der gesammelten Ansicht; die Eingabedatei wurde nicht verändert. Unterschiedliche IDs desselben Typs, Schäden und nicht eingeplante Ausrüstung blieben erhalten. Der nächste Schritt prüft Widersprüche, die das gesamte Sammlungsergebnis ablehnen müssen, statt stillschweigend den ersten oder letzten Datensatz zu wählen.

Repariere bei einem Compilerfehler die gemeldete Zeile in nano, gegebenenfalls auch einen fehlenden Import. Die folgende bedingte Wiederherstellung verwendet nur zum Rückgängigmachen eines gespeicherten Fehlers deine eigene Datei vor der Sammlung. Wiederhole in einem neuen Terminal PATH und cd und liste die tatsächlichen Sicherungsnamen der Phase auf:

ls -d .edit-before-identity.*

Führe read aus, füge einen exakt ausgegebenen Verzeichnisnamen ein und drücke Enter. Bestätige seinen Inhalt vor dem Wiederherstellen. Passt keine Sicherung, verwende die Referenz für eine gezielte Reparatur, statt einen Pfad zu erraten.

read rs03_before_identity
cat "$rs03_before_identity/identity.rs"

Stelle nach dem Bestätigen des beabsichtigten Quelltexts nur die erlaubte Identitätsdatei wieder her:

cp -p "$rs03_before_identity/identity.rs" src/identity.rs

Dies stellt den ursprünglichen Sammlungsplatzhalter samt ursprünglichen Imports wieder her. Ergänze den benötigten Import deiner Implementierung erneut und vervollständige die Funktion vor dem nächsten Test. Dein erfolgreicher Zustandsparser wird dadurch nicht entfernt und die Rohdaten ändern sich nicht.

Vollständige Identitätsreferenz für Vergleich und Wiederherstellung

Vergleiche nach deinem eigenen Implementierungsversuch die betreffende Entscheidung und repariere lokal in nano. Dies ist die einzige vollständige Identitätsquelle, die zum Vorbereiten späterer unabhängiger Einheiten verwendet wird.

use crate::Scan;
use std::collections::HashMap;

pub fn collect_assets(scans: &[Scan]) -> Result<Vec<Scan>, String> {
    let mut by_id: HashMap<String, Scan> = HashMap::new();
    for scan in scans {
        if let Some(previous) = by_id.get(&scan.asset_id) {
            if previous.type_id != scan.type_id || previous.condition != scan.condition {
                return Err(format!(
                    "line {}: conflicting asset {} (first at line {})",
                    scan.line, scan.asset_id, previous.line
                ));
            }
            continue;
        }
        by_id.insert(scan.asset_id.clone(), scan.clone());
    }
    let mut unique = Vec::new();
    for (_, scan) in by_id {
        unique.push(scan);
    }
    Ok(unique)
}

Konflikte ablehnen und nach Eingabefehlern fortfahren

In diesem Schritt unterscheidest du widersprüchliche Belege, einen unbekannten Zustand und einen falschen Pfad. Arbeite weiter in Terminal im Projektverzeichnis. Die folgenden Fehler sind beabsichtigt: Die Befehle sollen eine Fehlermeldung ausgeben und einen Exit-Status ungleich null liefern. Lösche oder repariere die erhaltenen Beispielquellen nicht, nur um die Fehler verschwinden zu lassen.

Die Fehlerbeispiele unten zeigen den Wortlaut der Referenzimplementierung. Deine eigene Meldung darf andere Wörter verwenden: Ein Konflikt muss den tatsächlichen Pfad, die vollständige Objekt-ID und beide Quellzeilen nennen; ein unbekannter Zustand muss seinen tatsächlichen Pfad und seine Zeile nennen sowie den abgelehnten Wert erklären. Der exakte Wortlaut ist nicht das Ziel.

Lies zunächst die widersprüchlichen Belege:

cat data/conflict.tsv

Sie beschreiben Objekt a gleichzeitig als good und damaged. Führe die Sammelfunktion aus:

cargo run --quiet -- assets data/conflict.tsv
data/conflict.tsv: line 3: conflicting asset a (first at line 2)

Die bereitgestellte Befehlsoberfläche ergänzt den Fehler deiner Sammelfunktion um den tatsächlichen Eingabepfad. Es wird keine Erfolgstabelle ausgegeben. Eine Implementierung, die den letzten Datensatz übernimmt, würde Belege verbergen und könnte ihr Ergebnis schon durch Umkehren der Datei ändern. Identische Wiederholungen sind erlaubt, Widersprüche benötigen jedoch eine Untersuchung außerhalb dieser Übung.

Wähle erneut die gültige Quelle, um fortzufahren:

cargo run --quiet -- assets data/scans.tsv

Lies nun absichtlich das Beispiel mit einem unbekannten Zustand:

cargo run --quiet -- rows data/bad-status.tsv
data/bad-status.tsv: line 2: condition must be good or damaged, got "broken"

Hier geht es um das Format, nicht um ein beschädigtes Objekt: damaged ist gültige Eingabe, während broken außerhalb des kontrollierten Kursvokabulars liegt. Lies einen gültigen Datensatz mit beschädigtem Objekt, um fortzufahren:

cargo run --quiet -- rows data/one-damaged.tsv

Verwende zuletzt absichtlich einen nicht vorhandenen Dateinamen im Singular:

cargo run --quiet -- assets data/scan.tsv

Die Fehlermeldung nennt data/scan.tsv und erklärt, dass die Datei nicht gefunden wurde. Das ist etwas anderes als ein Zeilenfehler in einer vorhandenen Datei. Korrigiere den Pfad und lasse die tatsächliche Quelle unverändert:

cargo run --quiet -- assets data/scans.tsv

Die Tabelle mit sechs Objekten sollte wieder erscheinen. Ist eine Rust-Bearbeitung falsch, öffne die genannte Quelle erneut in nano und repariere die betroffene Funktion. Verwende zum Rückgängigmachen eines gespeicherten Fehlers den Sicherungsstand der Zustands- oder Identitätsphase und das im früheren Schritt gelehrte ausdrückliche Bestätigungsverfahren mit ls/read/cat; stelle nur diese erlaubte Quelle wieder her, implementiere sie erneut und wiederhole diesen letzten Befehl. Ungespeicherte nano-Änderungen kannst du stattdessen mit Strg+X und N verwerfen. In keinem Fall musst du das Setup erneut ausführen oder eine geschützte Eingabe verändern. Dein Programm unterscheidet nun Quellfehler von gültigen wiederholten, beschädigten und zusätzlichen Datensätzen. Das nächste Lab verwendet diese eindeutigen Identitäten zur Berechnung von Fehlmengen.

Zusammenfassung

Du hast einen Parser für eine Enumeration mit zwei Zuständen implementiert und Ausrüstung anhand physischer Objekt-IDs gesammelt. Unterschiedliche Identitäten bleiben erhalten, identische Wiederholungen erhöhen die Anzahl nicht, und Widersprüche erzeugen Fehler mit Herkunftsangaben, statt Belege stillschweigend zu ersetzen. Du hast außerdem einen unbekannten Zustand und einen falschen Dateinamen korrigiert, ohne die Originaldateien zu verändern. Diese verlässlichen Objektdatensätze bilden die Grundlage, um Fehlmengen und Überschüsse je Typ zu erklären.