Definieren und Instanziieren von Structs

Beginner

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

Einführung

Willkommen zu Defining and Instantiating Structs. Dieser Lab ist Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab lernen wir, wie man in Rust Structs definiert und instanziiert. Structs halten mehrere zusammenhängende Werte und können benannte Felder haben, was eine flexiblere Verwendung und den Zugang zu Daten ermöglicht.

Definieren und Instanziieren von Structs

Structs ähneln Tuples, wie in "The Tuple Type" diskutiert, in dem beide mehrere zusammenhängende Werte speichern. Wie bei Tuples können die Elemente eines Structs unterschiedliche Typen sein. Anders als bei Tuples benennst du in einem Struct jedes Datenstück, sodass klar ist, was die Werte bedeuten. Das Hinzufügen dieser Namen bedeutet, dass Structs flexibler als Tuples sind: Du musst dich nicht auf die Reihenfolge der Daten verlassen, um die Werte einer Instanz anzugeben oder zuzugreifen.

Um einen Struct zu definieren, geben wir das Schlüsselwort struct ein und benennen den gesamten Struct. Der Name eines Structs sollte die Bedeutung der zusammengefassten Datenstücke beschreiben. Dann definieren wir innerhalb geschweifter Klammern die Namen und Typen der Datenstücke, die wir Felder nennen. Beispielsweise zeigt Listing 5-1 einen Struct, der Informationen über ein Benutzerkonto speichert.

Dateiname: src/main.rs

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

Listing 5-1: Eine User-Struct-Definition

Um einen Struct nach seiner Definition zu verwenden, erstellen wir eine Instanz dieses Structs, indem wir konkrete Werte für jedes Feld angeben. Wir erstellen eine Instanz, indem wir den Namen des Structs angeben und dann geschweifte Klammern hinzufügen, die Schlüssel: Wert-Paare enthalten, wobei die Schlüssel die Namen der Felder sind und die Werte die Daten sind, die wir in diese Felder speichern möchten. Wir müssen die Felder nicht in der gleichen Reihenfolge angeben, in der wir sie im Struct deklariert haben. Mit anderen Worten, die Struct-Definition ist wie eine allgemeine Vorlage für den Typ, und Instanzen füllen diese Vorlage mit bestimmten Daten aus, um Werte des Typs zu erstellen. Beispielsweise können wir einen bestimmten Benutzer wie in Listing 5-2 deklarieren.

Dateiname: src/main.rs

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
}

Listing 5-2: Erstellen einer Instanz des User-Structs

Um einen bestimmten Wert aus einem Struct zu erhalten, verwenden wir die Punktnotation. Beispielsweise verwenden wir user1.email, um die E-Mail-Adresse dieses Benutzers zuzugreifen. Wenn die Instanz änderbar ist, können wir einen Wert ändern, indem wir die Punktnotation verwenden und in ein bestimmtes Feld zuweisen. Listing 5-3 zeigt, wie man den Wert im email-Feld einer änderbaren User-Instanz ändert.

Dateiname: src/main.rs

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}

Listing 5-3: Ändern des Werts im email-Feld einer User-Instanz

Beachte, dass die gesamte Instanz änderbar sein muss; Rust erlaubt es uns nicht, nur bestimmte Felder als änderbar zu markieren. Wie bei jedem Ausdruck können wir eine neue Instanz des Structs als den letzten Ausdruck im Funktionskörper konstruieren, um diese neue Instanz implizit zurückzugeben.

Listing 5-4 zeigt eine build_user-Funktion, die eine User-Instanz mit der angegebenen E-Mail und dem Benutzernamen zurückgibt. Das active-Feld erhält den Wert true, und das sign_in_count erhält einen Wert von 1.

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}

Listing 5-4: Eine build_user-Funktion, die eine E-Mail und einen Benutzernamen übernimmt und eine User-Instanz zurückgibt

Es ergibt Sinn, die Funktionsparameter mit denselben Namen wie die Struct-Felder zu benennen, aber das Wiederholen der email- und username-Feldnamen und -variablen ist etwas lästig. Wenn der Struct mehr Felder hätte, würde das Wiederholen jedes Namens noch ärgerlich sein. Zum Glück gibt es eine praktische Abkürzung!

Verwendung der Kurzschreibweise für Feldinitialisierung

Da die Parameter-Namen und die Struct-Feldnamen in Listing 5-4 genau gleich sind, können wir die Syntax der Kurzschreibweise für Feldinitialisierung verwenden, um build_user umzuschreiben, sodass es genau das gleiche Verhalten hat, aber ohne die Wiederholung von username und email, wie in Listing 5-5 gezeigt.

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}

Listing 5-5: Eine build_user-Funktion, die die Kurzschreibweise für Feldinitialisierung verwendet, da die username- und email-Parameter den gleichen Namen wie die Struct-Felder haben

Hier erstellen wir eine neue Instanz des User-Structs, der ein Feld namens email hat. Wir möchten den Wert des email-Felds auf den Wert des email-Parameters der build_user-Funktion setzen. Da das email-Feld und der email-Parameter den gleichen Namen haben, müssen wir nur email schreiben, anstatt email: email.

Erstellen von Instanzen aus anderen Instanzen mit der Struct-Update-Syntax

Es ist oft nützlich, eine neue Instanz eines Structs zu erstellen, die die meisten Werte aus einer anderen Instanz enthält, aber einige ändert. Dies kannst du mit der Struct-Update-Syntax tun.

Zunächst zeigen wir in Listing 5-6, wie man normalerweise eine neue User-Instanz in user2 erstellt, ohne die Update-Syntax. Wir setzen einen neuen Wert für email, verwenden aber sonst die gleichen Werte aus user1, die wir in Listing 5-2 erstellt haben.

Dateiname: src/main.rs

fn main() {
    --snip--

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}

Listing 5-6: Erstellen einer neuen User-Instanz, indem man einen der Werte aus user1 verwendet

Mit der Struct-Update-Syntax können wir das gleiche Ergebnis mit weniger Code erreichen, wie in Listing 5-7 gezeigt. Die Syntax .. gibt an, dass die verbleibenden Felder, die nicht explizit festgelegt werden, den gleichen Wert wie die Felder in der angegebenen Instanz haben sollen.

Dateiname: src/main.rs

fn main() {
    --snip--


    let user2 = User {
        email: String::from("another@example.com"),
      ..user1
    };
}

Listing 5-7: Verwenden der Struct-Update-Syntax, um einen neuen email-Wert für eine User-Instanz festzulegen, aber die restlichen Werte aus user1 zu verwenden

Der Code in Listing 5-7 erstellt auch eine Instanz in user2, die einen anderen Wert für email hat, aber die gleichen Werte für die username, active und sign_in_count-Felder aus user1 hat. Das ..user1 muss zuletzt stehen, um anzugeben, dass alle verbleibenden Felder ihre Werte aus den entsprechenden Feldern in user1 erhalten sollen, aber wir können uns entscheiden, beliebig viele Felder in beliebiger Reihenfolge Werte zuzuweisen, unabhängig von der Reihenfolge der Felder in der Struct-Definition.

Beachte, dass die Struct-Update-Syntax = wie eine Zuweisung verwendet; dies liegt daran, dass sie die Daten bewegt, genau wie wir es in "Variablen und Daten, die mit Move interagieren" gesehen haben. In diesem Beispiel können wir user1 nicht mehr verwenden, nachdem wir user2 erstellt haben, weil die String im username-Feld von user1 in user2 bewegt wurde. Wenn wir user2 sowohl für email als auch für username neue String-Werte gegeben hätten und somit nur die active- und sign_in_count-Werte aus user1 verwendet hätten, wäre user1 nach dem Erstellen von user2 immer noch gültig. Sowohl active als auch sign_in_count sind Typen, die das Copy-Trait implementieren, sodass das Verhalten, das wir in "Nur auf dem Stack gespeicherte Daten: Copy" diskutiert haben, anwendbar wäre.

Verwendung von Tuple Structs ohne benannte Felder, um verschiedene Typen zu erstellen

Rust unterstützt auch Structs, die ähnlich wie Tuples aussehen, sogenannte Tuple Structs. Tuple Structs haben die zusätzliche Bedeutung, die der Struct-Name bietet, aber haben keine Namen, die mit ihren Feldern assoziiert sind; stattdessen haben sie nur die Typen der Felder. Tuple Structs sind nützlich, wenn Sie dem gesamten Tuple einen Namen geben möchten und das Tuple einen anderen Typ als andere Tuples machen möchten, und wenn das Benennen jedes Felds wie in einem normalen Struct umständlich oder redundant wäre.

Um einen Tuple Struct zu definieren, beginnen Sie mit dem Schlüsselwort struct und dem Struct-Namen, gefolgt von den Typen im Tuple. Beispielsweise definieren und verwenden wir hier zwei Tuple Structs namens Color und Point:

Dateiname: src/main.rs

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Beachte, dass die black- und origin-Werte unterschiedliche Typen sind, weil sie Instanzen unterschiedlicher Tuple Structs sind. Jeder Struct, den Sie definieren, ist ein eigener Typ, auch wenn die Felder innerhalb des Structs möglicherweise die gleichen Typen haben. Beispielsweise kann eine Funktion, die einen Parameter vom Typ Color annimmt, keinen Point als Argument akzeptieren, auch wenn beide Typen aus drei i32-Werten bestehen. Andernfalls sind Tuple Struct-Instanzen ähnlich wie Tuples, in dem Sie sie in ihre einzelnen Teile zerlegen können, und Sie können ein . gefolgt von dem Index verwenden, um einen einzelnen Wert zuzugreifen.

Strukturen ohne Felder, ähnlich der Einheit

Es ist auch möglich, Structs zu definieren, die keine Felder haben! Diese werden als einheitsähnliche Structs bezeichnet, weil sie ähnlich wie (), dem Einheitstyp, den wir in "The Tuple Type" erwähnt haben, verhalten. Einheitsähnliche Structs können nützlich sein, wenn Sie ein Merkmal auf einem bestimmten Typ implementieren müssen, aber keine Daten haben, die Sie in dem Typ selbst speichern möchten. Wir werden in Kapitel 10 über Merkmale sprechen. Hier ist ein Beispiel für die Deklaration und Instanziierung eines einheitlichen Structs namens AlwaysEqual:

Dateiname: src/main.rs

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Um AlwaysEqual zu definieren, verwenden wir das Schlüsselwort struct, den Namen, den wir möchten, und dann ein Semikolon. Keine geschweiften Klammern oder Klammern erforderlich! Dann können wir in ähnlicher Weise eine Instanz von AlwaysEqual in der subject-Variablen erhalten: indem wir den definierten Namen verwenden, ohne jede geschweifte Klammer oder Klammer. Stellen Sie sich vor, dass wir später ein Verhalten für diesen Typ implementieren, sodass jede Instanz von AlwaysEqual immer gleich ist wie jede Instanz eines anderen Typs, vielleicht um ein bekanntes Ergebnis für Testzwecke zu haben. Wir bräuchten keine Daten, um dieses Verhalten zu implementieren! In Kapitel 10 werden Sie sehen, wie Sie Merkmale definieren und auf jeden Typ implementieren, einschließlich einheitsähnlicher Structs.

Eigentum an Struct-Daten

In der User-Struct-Definition in Listing 5-1 haben wir den eigenen String-Typ verwendet, statt des &str-String-Slices-Typs. Dies ist eine bewusste Wahl, weil wir möchten, dass jede Instanz dieses Structs alle ihre Daten besitzt und dass diese Daten solange gültig sind, wie der gesamte Struct gültig ist.

Es ist auch möglich, dass Structs Referenzen auf Daten speichern, die von etwas anderem besessen werden, aber dazu muss das Konzept der Lebensdauer verwendet werden, ein Rust-Feature, über das wir in Kapitel 10 sprechen werden. Lebensdauern gewährleisten, dass die von einem Struct referenzierten Daten solange gültig sind, wie der Struct selbst. Nehmen wir an, Sie versuchen, eine Referenz in einem Struct zu speichern, ohne die Lebensdauer anzugeben, wie im folgenden Beispiel in src/main.rs; dies wird nicht funktionieren:

struct User {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: "someusername123",
        email: "someone@example.com",
        sign_in_count: 1,
    };
}

Der Compiler wird anzeigen, dass Lebensdauerangaben erforderlich sind:

$ `cargo run`
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

In Kapitel 10 werden wir diskutieren, wie diese Fehler behoben werden können, sodass Sie Referenzen in Structs speichern können, aber für jetzt werden wir Fehler wie diese mit eigenen Typen wie String anstelle von Referenzen wie &str beheben.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab Defining and Instantiating Structs abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.