< Ownership<  |    |  Enums >>

Structs

Was sind Structs

Structs kennt man ja aus C/C++. Man kann es (denke ich) auch mit JavaScript Objekten vergleichen.

In Structs gruppiert man zusammengehöriges Zeug und hat so eine Art Pseudo-OOP. Man kann damit neue Datentypen machen.

How to

"Normale" Structs

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

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

    println!("{}", user1.email);

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

Hinweis: Es können nicht einzelne Felder mutable sein, sondern wenn dann immer das ganze Struct.

Dinge wie in Javascript

Wenn die Variable heißt wie das Feld, kann man auch statt email: email einfach nur email schreiben.

Wenn man ein neues Struct aus einem alten mit Updates erstellen will, geht das auch mit einer Art Spread-Parameter:

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

..user1 muss als letztes kommen und füllt dann alle bisher nicht gesetzten Felder. Außerdem ist das etwas tricky: Wenn die Daten, die von user1 zu user2 übertragen werden, gemoved werden (sprich: keine primitiven Datentypen sind), dann ist user1 danach ungültig. Hätten wir jetzt auch noch einen neuen username gesetzt (auch ein String) und nur active und sign_in_count übertragen, wäre user1 noch gültig.

Tupel Structs

struct RGBColor(u8, u8, u8);

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

Sind nutzbar wie Tupel (destrucuture und .index zum Zugriff auf Werte), allerdings eben ein eigener Typ.

Unit-Like Structs

struct AlwaysEqual;

Ein Struct muss keine Felder haben. Das Buch meint, man könnte für diesen Datentypen jetzt noch Traits implementieren, aber davon habe ich noch keine Ahnung. Nur dann macht diese Art von Struct irgendwie Sinn.

Ownership der Felder

Im ersten Beispiel wird String satt &str genutzt. Wir wollen am besten im Struct keine Referenzen, oder es müssen "named lifetime parameter" sein, etwas das wir erst später lernen. Der Compiler wird sonst streiken.

Das erste Mal Traits

Im Buch folgt ein Beispielprogramm für ein Struct, das ein Rechteck abbildet. Wir wollten das ganze printen (mit {} als Platzhalter), allerdings implementiert Das Rechteck nicht std::fmt::Display. Das scheint eine Art toString() für Rust zu sein.

Es gibt aber noch eine andere Möglichkeit und das haben wir schonmal für Tupel genutzt: {:?} als Platzhalter (bzw. {:#?} für pretty print). Dafür brauchen wir aber das Trait Debug. Zum Glück scheint das aber einfach zu implementieren sein, es muss nur implementiert werden.

Der Compiler schlägt uns zwei Varianten vor:

  1. #[derive(Debug)] über der Definition des Structs

  2. impl Debug for Rectangle manuell

Jetzt können wir Variablen dieses Typs printen und es zeigt uns Datentyp und Felder an.

Alternativ kann man auch das Makro dbg!(…​) nutzen. Das wird dann auf stderr geprintet. Man kann sogar ein Statement da rein packen (also zum Beispiel 30 * x) und bekommt das Statement mit dem Ergebnis geprintet, wobei das Ergebnis (als Wert, nicht Referenz) auch zurückgegeben wird.

Funktionen in Structs

Unser Struct soll jetzt auch eine Funktion auf sich selbst aufrufen können. Tatsächlich ist der sehr einfach und sehr OOPig.

Die folgenden Beispiele sollten relativ viel erklären:

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

impl Rectangle {
    // Das ist eine Methode/"method"
    // Erster Parameter ist &self (/&mut self) und wird aufgerufen wie folgt:
    // var.area();
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // Das ist eine "associated function"
    // Kein &self und aufgerufen wie folgt:
    // Rectangle::square(5);
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

// Mehrere impl Blöcke sind erlaubt
impl Rectangle {
    // var.has_same_area(&other);
    fn has_same_area(&self, other: &Rectangle) -> bool {
        self.area() == other.area()
    }

    // Rectangle::same_area(&first, &second);
    fn same_area(first: &Rectangle, second: &Rectangle) -> bool {
        first.area() == second.area()
    }

    // Methoden können auch wie Felder heißen
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 12,
        height: 3,
    };
    let rect2 = Rectangle::square(6);

    println!("{}", rect1.area()); // 36
    println!("{}", rect2.area()); // 36

    println!("{}", rect1.has_same_area(&rect2)); // true
    println!("{}", rect2.has_same_area(&rect1)); // true
    println!("{}", Rectangle::same_area(&rect1, &rect2)); // true
}

&mut self

Eine Methode kann auch &mut self als ersten Parameter haben. Dann können auch Felder geschrieben werden. In diesem Fall werden Referenzen aber invalidiert!

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

impl Rectangle {
    fn change_width(&mut self, width: u32) {
        self.width = width;
    }
}

fn main() {
    let mut rect1 = Rectangle {
        width: 12,
        height: 3,
    };
    let ref1 = &rect1;
    rect1.change_width(5);

    println!("{}", ref1.width); // <- geht nicht!
}

< Ownership<  |    |  Enums >>