Ein Beispielprogramm mit Structs

Beginner

This tutorial is from open-source community. Access the source code

Einführung

Willkommen zu An Example Program Using Structs. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir ein Programm schreiben, das mit Structs die Fläche eines Rechtecks berechnet und den ursprünglichen Code umgestalten, der für Breite und Höhe separate Variablen verwendet hat.

Ein Beispielprogramm mit Structs

Um zu verstehen, wann wir Structs verwenden möchten, schreiben wir ein Programm, das die Fläche eines Rechtecks berechnet. Wir beginnen mit einzelnen Variablen und refaktorisieren das Programm, bis wir Structs verwenden.

Lassen Sie uns mit Cargo ein neues binäres Projekt namens rectangles erstellen, das die Breite und Höhe eines Rechtecks in Pixeln angibt und die Fläche des Rechtecks berechnet. Listing 5-8 zeigt ein kurzes Programm, das genau das auf eine Weise in unserer Projekt-src/main.rs macht.

Dateiname: src/main.rs

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "Die Fläche des Rechtecks beträgt {} Quadratpixel.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

Listing 5-8: Berechnung der Fläche eines Rechtecks, das durch separate Breite- und Höhe-Variablen angegeben wird

Führen Sie nun dieses Programm mit cargo run aus:

Die Fläche des Rechtecks beträgt 1500 Quadratpixel.

Dieser Code gelangt erfolgreich zu der Fläche des Rechtecks, indem er die area-Funktion mit jeder Dimension aufruft, aber wir können noch mehr tun, um diesen Code klar und lesbar zu machen.

Das Problem mit diesem Code ist in der Signatur von area offensichtlich:

fn area(width: u32, height: u32) -> u32 {

Die area-Funktion soll die Fläche eines Rechtecks berechnen, aber die von uns geschriebene Funktion hat zwei Parameter, und es ist in unserem gesamten Programm nirgends klar, dass die Parameter zusammenhängen. Es wäre lesbarer und leichter zu verwalten, Breite und Höhe zusammen zu gruppieren. Wir haben bereits eine Möglichkeit diskutiert, wie wir das in "Der Tuple-Typ" tun könnten: indem wir Tupel verwenden.

Refactoring mit Tupeln

Listing 5-9 zeigt eine andere Version unseres Programms, das Tupel verwendet.

Dateiname: src/main.rs

fn main() {
    let rect1 = (30, 50);

    println!(
        "Die Fläche des Rechtecks beträgt {} Quadratpixel.",
      1 area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
  2 dimensions.0 * dimensions.1
}

Listing 5-9: Angabe der Breite und Höhe des Rechtecks mit einem Tupel

Auf eine Weise ist dieses Programm besser. Tupel erlauben es uns, etwas Struktur hinzuzufügen, und wir übergeben jetzt nur einen Argument [1]. Aber auf eine andere Weise ist diese Version weniger klar: Tupel benennen ihre Elemente nicht, sodass wir in die Teile des Tupels indexieren müssen [2], was unsere Berechnung weniger offensichtlich macht.

Das Vertauschen von Breite und Höhe würde für die Flächenberechnung keine Rolle spielen, aber wenn wir das Rechteck auf dem Bildschirm zeichnen möchten, würde es eine Rolle spielen! Wir müssten dann im Kopf behalten, dass width der Tupelindex 0 ist und height der Tupelindex 1 ist. Es wäre noch schwieriger für jemanden anderen, das herauszufinden und sich zu merken, wenn er unseren Code verwenden würde. Da wir die Bedeutung unserer Daten in unserem Code nicht vermittelt haben, ist es jetzt einfacher, Fehler zu machen.

Refactoring mit Structs: Hinzufügen von mehr Bedeutung

Wir verwenden Structs, um Bedeutung hinzuzufügen, indem wir die Daten benennen. Wir können das Tupel, das wir verwenden, in einen Struct umwandeln, der einen Namen für das Ganze sowie Namen für die Teile hat, wie in Listing 5-10 gezeigt.

Dateiname: src/main.rs

1 struct Rectangle {
  2 width: u32,
    height: u32,
}

fn main() {
  3 let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "Die Fläche des Rechtecks beträgt {} Quadratpixel.",
        area(&rect1)
    );
}

4 fn area(rectangle: &Rectangle) -> u32 {
  5 rectangle.width * rectangle.height
}

Listing 5-10: Definition eines Rectangle-Structs

Hier haben wir einen Struct definiert und ihn Rectangle genannt [1]. Innerhalb der geschweiften Klammern haben wir die Felder als width und height definiert, beide von Typ u32 [2]. Dann haben wir in main eine bestimmte Instanz von Rectangle erstellt, die eine Breite von 30 und eine Höhe von 50 hat [3].

Unsere area-Funktion ist jetzt mit einem Parameter definiert, den wir rectangle genannt haben, dessen Typ eine unveränderliche Referenz auf eine Struct-Rectangle-Instanz ist [4]. Wie in Kapitel 4 erwähnt, möchten wir die Struct referenzieren, anstatt die Eigentumsgewalt zu übernehmen. Auf diese Weise behält main seine Eigentumsgewalt und kann weiterhin rect1 verwenden, was der Grund ist, warum wir das & in der Funktionssignatur und beim Funktionsaufruf verwenden.

Die area-Funktion greift auf die width- und height-Felder der Rectangle-Instanz zu [5] (beachten Sie, dass das Zugreifen auf Felder einer referenzierten Struct-Instanz die Feldwerte nicht bewegt, weshalb Sie oft Referenzen auf Structs sehen). Unsere Funktionssignatur für area sagt jetzt genau, was wir meinen: Berechnen Sie die Fläche von Rectangle, indem Sie seine width- und height-Felder verwenden. Dies vermittelt, dass Breite und Höhe miteinander zusammenhängen, und es gibt beschreibende Namen für die Werte anstelle von den Tupelindexwerten 0 und 1. Dies ist ein Gewinn für die Klarheit.

Hinzufügen von nützlicher Funktionalität mit abgeleiteten Traits

Es wäre nützlich, während des Debuggings unseres Programms eine Rectangle-Instanz ausdrucken zu können und die Werte aller ihrer Felder anzeigen zu sehen. Listing 5-11 versucht, die println!-Makro wie in vorherigen Kapiteln zu verwenden. Dies funktioniert jedoch nicht.

Dateiname: src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 ist {}", rect1);
}

Listing 5-11: Versuch, eine Rectangle-Instanz auszugeben

Wenn wir diesen Code kompilieren, erhalten wir einen Fehler mit dieser Kernmeldung:

error[E0277]: `Rectangle` implementiert `std::fmt::Display` nicht

Das println!-Makro kann viele Arten von Formatierung durchführen, und standardmäßig sagen die geschweiften Klammern println!, dass es die Formatierung Display verwenden soll: Ausgabe, die für den direkten Endbenutzer bestimmt ist. Die primitiven Typen, die wir bisher gesehen haben, implementieren Display standardmäßig, da es nur eine Möglichkeit gibt, eine 1 oder irgendeinen anderen primitiven Typ einem Benutzer zu zeigen. Bei Structs ist jedoch die Art, wie println! die Ausgabe formatieren soll, weniger klar, da es mehr Anzeigemöglichkeiten gibt: Möchten Sie Kommas oder nicht? Möchten Sie die geschweiften Klammern ausgeben? Sollen alle Felder angezeigt werden? Aufgrund dieser Mehrdeutigkeit versucht Rust nicht, zu erraten, was wir möchten, und Structs haben keine bereitgestellte Implementierung von Display, um mit println! und dem {}-Platzhalter zu verwenden.

Wenn wir die Fehler weiter lesen, finden wir diese hilfreiche Anmerkung:

= help: das Trait `std::fmt::Display` ist für `Rectangle` nicht implementiert
= note: in Formatstrings können Sie möglicherweise `{:?}` (oder {:#?} für
schöne Ausgabe) verwenden

Lassen Sie uns es ausprobieren! Der println!-Makroaufruf sieht jetzt so aus: println!("rect1 ist {:?}", rect1);. Indem wir den Spezifizierer :? in die geschweiften Klammern setzen, sagen wir println!, dass wir ein Ausgabeformat namens Debug verwenden möchten. Das Debug-Trait ermöglicht es uns, unsere Struct auf eine Weise auszugeben, die für Entwickler nützlich ist, sodass wir ihren Wert während des Debuggings unseres Codes sehen können.

Kompilieren Sie den Code mit dieser Änderung. Verdammt! Wir erhalten immer noch einen Fehler:

error[E0277]: `Rectangle` implementiert `Debug` nicht

Aber wiederum gibt uns der Compiler eine hilfreiche Anmerkung:

= help: das Trait `Debug` ist für `Rectangle` nicht implementiert
= note: fügen Sie `#[derive(Debug)]` hinzu oder implementieren Sie `Debug` manuell

Rust hat tatsächlich Funktionalität, um Debuginformationen auszugeben, aber wir müssen explizit aktivieren, um diese Funktionalität für unsere Struct zur Verfügung zu stellen. Dazu fügen wir das äußere Attribut #[derive(Debug)] direkt vor der Struct-Definition hinzu, wie in Listing 5-12 gezeigt.

Dateiname: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 ist {:?}", rect1);
}

Listing 5-12: Hinzufügen des Attributes, um das Debug-Trait abzuleiten und die Rectangle-Instanz mit Debug-Formatierung auszugeben

Wenn wir jetzt das Programm ausführen, erhalten wir keine Fehler mehr, und wir sehen die folgende Ausgabe:

rect1 ist Rectangle { width: 30, height: 50 }

Super! Es ist nicht die schönste Ausgabe, aber sie zeigt die Werte aller Felder für diese Instanz, was definitiv bei der Fehlersuche helfen würde. Wenn wir größere Structs haben, ist es nützlich, eine Ausgabe zu haben, die etwas leichter lesbar ist; in diesen Fällen können wir in der println!-Zeichenfolge {:#?} statt {:?} verwenden. In diesem Beispiel wird die Ausgabe mit dem {:#?}-Format wie folgt aussehen:

rect1 ist Rectangle {
    width: 30,
    height: 50,
}

Eine andere Möglichkeit, einen Wert im Debug-Format auszugeben, ist die Verwendung des dbg!-Makros, das die Eigentumsgewalt eines Ausdrucks übernimmt (im Gegensatz zu println!, das eine Referenz nimmt), druckt die Datei und die Zeilennummer, an der der dbg!-Makroaufruf in Ihrem Code auftritt, zusammen mit dem resultierenden Wert dieses Ausdrucks und gibt die Eigentumsgewalt des Werts zurück.

Hinweis: Der Aufruf des dbg!-Makros druckt auf den Standardfehlerkonsolenstrom (stderr), im Gegensatz zu println!, das auf den Standardausgabekonsolenstrom (stdout) druckt. Wir werden in "Schreiben von Fehlermeldungen an die Standardfehlerstelle anstelle der Standardausgabe" mehr über stderr und stdout sprechen.

Hier ist ein Beispiel, in dem wir an den Wert interessiert sind, der dem width-Feld zugewiesen wird, sowie an dem Wert der gesamten Struct in rect1:

Dateiname: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
      1 width: dbg!(30 * scale),
        height: 50,
    };

  2 dbg!(&rect1);
}

Wir können dbg! um den Ausdruck 30 * scale legen [1], und da dbg! die Eigentumsgewalt des Ausdrucks' Werts zurückgibt, wird das width-Feld den gleichen Wert erhalten wie, wenn wir den dbg!-Aufruf dort nicht hätten. Wir möchten nicht, dass dbg! die Eigentumsgewalt von rect1 übernimmt, daher verwenden wir in dem nächsten Aufruf eine Referenz auf rect1 [2]. Hier sieht die Ausgabe dieses Beispiels aus:

[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Wir können sehen, dass der erste Ausgabeanteil von [1] stammt, wo wir den Ausdruck 30 * scale debuggen, und sein resultierender Wert ist 60 (die für Integer implementierte Debug-Formatierung ist es, nur ihren Wert auszugeben). Der dbg!-Aufruf bei [2] gibt den Wert von &rect1 aus, was die Rectangle-Struct ist. Diese Ausgabe verwendet die schöne Debug-Formatierung des Rectangle-Typs. Das dbg!-Makro kann wirklich hilfreich sein, wenn Sie versuchen, herauszufinden, was Ihr Code tut!

Neben dem Debug-Trait hat Rust eine Reihe von Traits für uns bereitgestellt, die wir mit dem derive-Attribut verwenden können, um nützliches Verhalten zu unseren benutzerdefinierten Typen hinzuzufügen. Diese Traits und ihr Verhalten sind in Anhang C aufgelistet. Wir werden im Kapitel 10 auch besprechen, wie man diese Traits mit benutzerdefiniertem Verhalten implementiert und wie man eigene Traits erstellt. Es gibt auch viele Attribute außer derive; für weitere Informationen siehe den Abschnitt "Attribute" in der Rust-Referenz unter https://doc.rust-lang.org/reference/attributes.html.

Unsere area-Funktion ist sehr spezifisch: sie berechnet nur die Fläche von Rechtecken. Es wäre nützlich, dieses Verhalten enger an unsere Rectangle-Struct zu binden, da es mit keinem anderen Typ funktionieren wird. Schauen wir uns an, wie wir diesen Code weiter refaktorisieren können, indem wir die area-Funktion in eine area-Methode definieren, die auf unserem Rectangle-Typ definiert ist.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "An Example Program Using Structs" abgeschlossen. Sie können in LabEx weitere Labs ausprobieren, um Ihre Fähigkeiten zu verbessern.