Wichtige Traits in Rust
Auf der Seite Traits hast du eigene Traits geschrieben. Die Standardbibliothek bringt aber schon eine Reihe von Traits mit, die fast jedes Rust-Programm benutzt. Sie beantworten einfache Fragen über einen Typ: Kann ich ihn ausgeben? Kann ich ihn kopieren? Kann ich ihn vergleichen? Kann ich einen leeren erzeugen?
Stell sie dir wie Abzeichen auf einer Uniform vor. Jedes Abzeichen sagt: „Dieser Typ kann X.“ Viele davon bekommst du mit #[derive(...)] geschenkt. Andere schreibst du von Hand. Diese Seite stellt dir die Traits vor, die dir am häufigsten begegnen, und endet mit einer Tabelle zum Nachschlagen.
Debug: Ausgabe für Entwickler
Abschnitt betitelt „Debug: Ausgabe für Entwickler“Mit Debug kannst du einen Wert mit {:?} ausgeben. Das ist für dich als Entwickler gedacht – um Werte zu prüfen, während du deinen Code schreibst und Fehler suchst. Von Hand schreibst du das fast nie; du leitest es einfach mit derive ab. Mit {:#?} bekommst du eine „hübsche“ Version mit einem Feld pro Zeile. Bei großen Structs ist die viel leichter zu lesen:
#[derive(Debug)]struct Book { title: String, pages: u32,}
fn main() { let book = Book { title: String::from("Dune"), pages: 412 }; println!("{:?}", book); println!("{:#?}", book);}Ausgabe:
Book { title: "Dune", pages: 412 }Book { title: "Dune", pages: 412,}Auch das Makro dbg!, das du vielleicht in anderem Code siehst, benutzt Debug.
Display: Ausgabe für Benutzer
Abschnitt betitelt „Display: Ausgabe für Benutzer“Display ist das, was {} benutzt. Es ist für Text, den Benutzer sehen – die Meldung, die ein Mensch auf dem Bildschirm liest. Wie die aussehen soll, kann Rust nicht erraten. Deshalb gibt es kein derive dafür. Du schreibst es selbst, indem du fmt::Display implementierst:
use std::fmt;
struct Book { title: String, author: String,}
impl fmt::Display for Book { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} by {}", self.title, self.author) }}
fn main() { let book = Book { title: String::from("Dune"), author: String::from("Frank Herbert"), };
println!("{}", book);
let text = book.to_string(); println!("Der Text hat {} Zeichen", text.len());}Ausgabe:
Dune by Frank HerbertDer Text hat 21 ZeichenDas "{} by {}" bleibt hier auf Englisch, weil das Programm danach die Länge des Textes misst. .len() zählt Bytes, und eine Übersetzung würde die Zahl in der Ausgabe verändern.
write! funktioniert wie println!, schreibt aber in den Formatter f statt auf den Bildschirm. Ein netter Bonus: Sobald ein Typ Display hat, bekommst du .to_string() geschenkt. Display begegnet dir wieder auf der Seite Eigene Fehlertypen, wo es Fehlern eine freundliche Meldung gibt.
Clone und Copy
Abschnitt betitelt „Clone und Copy“Beide erzeugen ein Duplikat eines Werts, aber auf unterschiedliche Weise.
Clonegibt dir eine Methode.clone(). Die rufst du bewusst auf, und sie kann echte Arbeit leisten – zum Beispiel den ganzen Text einesStringkopieren.Copyerzeugt automatisch ein Duplikat, jedes Mal wenn du den Wert zuweist oder übergibst. Das ist nur für kleine, einfache Typen erlaubt, die sich Bit für Bit kopieren lassen: Zahlen,bool,charund Structs, die nur aus solchen Typen bestehen.
#[derive(Debug, Clone, Copy)]struct Point { x: i32, y: i32,}
#[derive(Debug, Clone)]struct User { name: String,}
fn main() { let p1 = Point { x: 1, y: 2 }; let p2 = p1; // Copy: p1 ist weiterhin benutzbar println!("{:?} und {:?}", p1, p2);
let u1 = User { name: String::from("Ana") }; let u2 = u1.clone(); // Clone: wir bitten ausdrücklich um eine Kopie println!("{:?} und {:?}", u1, u2);}Ausgabe:
Point { x: 1, y: 2 } und Point { x: 1, y: 2 }User { name: "Ana" } und User { name: "Ana" }Ohne Copy würde let p2 = p1; den Wert p1 verschieben (move), und du könntest p1 danach nicht mehr benutzen (siehe Ownership). Beachte: Copy braucht immer auch Clone. Deshalb leitest du beide zusammen ab.
PartialEq und Eq: vergleichen mit ==
Abschnitt betitelt „PartialEq und Eq: vergleichen mit ==“Mit PartialEq kannst du == und != benutzen. Leitest du es ab, sind zwei Werte gleich, wenn alle ihre Felder gleich sind:
#[derive(Debug, PartialEq)]struct Point { x: i32, y: i32,}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; let c = Point { x: 5, y: 0 };
println!("a == b: {}", a == b); println!("a != c: {}", a != c);}Ausgabe:
a == b: truea != c: trueUnd was ist Eq? Es bringt keine neuen Methoden mit. Es ist ein Versprechen: „Jeder Wert ist immer gleich sich selbst.“ Das klingt selbstverständlich, aber Gleitkommazahlen halten es nicht ein – in Rust ist f64::NAN == f64::NAN gleich false. Deshalb hat f64 zwar PartialEq, aber kein Eq. Du brauchst Eq (plus Hash), um deinen Typ als Schlüssel in einer HashMap oder in einem HashSet zu verwenden. Hat dein Struct keine Gleitkomma-Felder, leite einfach beide ab.
PartialOrd und Ord: vergleichen mit < und >
Abschnitt betitelt „PartialOrd und Ord: vergleichen mit < und >“Mit PartialOrd kannst du <, >, <= und >= benutzen. Ord fügt eine vollständige Ordnung hinzu. Genau die brauchen .sort(), .max() und .min(). Wenn du beide ableitest, vergleicht Rust die Felder von oben nach unten: zuerst das erste Feld, und nur wenn das gleich ist, das nächste.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]struct Version { major: u32, minor: u32, patch: u32,}
fn main() { let old = Version { major: 1, minor: 2, patch: 0 }; let new = Version { major: 1, minor: 10, patch: 3 }; println!("old < new: {}", old < new);
let mut versions = vec![ Version { major: 2, minor: 0, patch: 0 }, new, old, ]; versions.sort();
for v in &versions { println!("{}.{}.{}", v.major, v.minor, v.patch); }}Ausgabe:
old < new: true1.2.01.10.32.0.0Weil die Felder der Reihe nach verglichen werden, spielt die Reihenfolge der Felder eine Rolle. Hier ist major am wichtigsten – genau richtig für Versionsnummern. Bei Enums gelten die zuerst aufgeführten Varianten als „kleiner“. Und wie bei Eq bekommen Gleitkommazahlen nur PartialOrd, nicht Ord. Wie du sie trotzdem sortierst, zeigt die Seite Sortieren und Suchen.
Default: ein sinnvoller Startwert
Abschnitt betitelt „Default: ein sinnvoller Startwert“Default gibt deinem Typ eine Funktion ::default(), die einen „Startwert“ erzeugt. Beim Ableiten bekommt jedes Feld seinen eigenen Standardwert: 0 für Zahlen, false für bool, einen leeren String, einen leeren Vec. Mit der Schreibweise ..Default::default() setzt du ein paar Felder selbst und lässt den Rest auffüllen:
#[derive(Debug, Default)]struct Settings { volume: u32, muted: bool, username: String,}
fn main() { let basic = Settings::default(); println!("{:?}", basic);
let custom = Settings { volume: 80, ..Default::default() }; println!("{:?}", custom);}Ausgabe:
Settings { volume: 0, muted: false, username: "" }Settings { volume: 80, muted: false, username: "" }Möchtest du andere Startwerte (sagen wir, Lautstärke 50), lass das derive weg und schreib impl Default for Settings selbst – mit einer fn default() -> Self, die die gewünschten Werte zurückgibt.
From und Into: zwischen Typen umwandeln
Abschnitt betitelt „From und Into: zwischen Typen umwandeln“From beschreibt, wie man einen Typ aus einem anderen erzeugt. Das hast du schon benutzt: String::from("hi"). Wenn du From implementierst, schenkt dir Rust Into für die Gegenrichtung:
struct Celsius(f64);struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit { fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) }}
fn main() { let boiling = Fahrenheit::from(Celsius(100.0)); println!("Wasser kocht bei {}°F", boiling.0);
let body: Fahrenheit = Celsius(37.0).into(); println!("Die Körpertemperatur beträgt {:.1}°F", body.0);}Ausgabe:
Wasser kocht bei 212°FDie Körpertemperatur beträgt 98.6°FBei .into() muss Rust den Zieltyp kennen. Deshalb steht dort let body: Fahrenheit. (Rust gibt Kommazahlen übrigens immer mit Punkt aus, also 98.6 statt 98,6.)
Wie ? den Trait From benutzt. Hier spielt From seine wahre Stärke aus. Wenn der ?-Operator auf einen Fehler trifft, ruft er From::from auf, um diesen Fehler in den Fehlertyp umzuwandeln, den deine Funktion zurückgibt. Implementierst du also From für deinen eigenen Fehlertyp, wandelt ? die Fehler für dich um:
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
fn parse_age(text: &str) -> Result<u32, AppError> { let age: u32 = text.parse()?; // hier wird aus ParseIntError ein AppError Ok(age)}
fn main() { println!("{:?}", parse_age("42")); println!("{:?}", parse_age("abc"));}Ausgabe:
Ok(42)Err(BadNumber(ParseIntError { kind: InvalidDigit }))Die Seite Eigene Fehlertypen baut diese Idee noch weiter aus.
Operatoren überladen mit Add
Abschnitt betitelt „Operatoren überladen mit Add“In Rust ist a + b eigentlich ein Aufruf der Methode add aus dem Trait std::ops::Add. Implementierst du ihn für deinen Typ, funktioniert + einfach. type Output legt fest, welchen Typ die Addition ergibt:
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Point { x: i32, y: i32,}
impl Add for Point { type Output = Point;
fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } }}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 3, y: 4 }; println!("{:?}", a + b);}Ausgabe:
Point { x: 4, y: 6 }Dieselbe Idee funktioniert für Sub (-), Mul (*), Neg (einstelliges -) und AddAssign (+=). Setz das ein, wenn die Bedeutung offensichtlich ist – zwei Punkte addieren oder zwei Geldbeträge. Müsste jemand beim Lesen raten, was + für deinen Typ bedeutet, ist eine normale Methode mit einem klaren Namen die bessere Wahl.
Welche Traits sollte ich ableiten?
Abschnitt betitelt „Welche Traits sollte ich ableiten?“Hier eine kurze Übersicht für ein typisches Struct:
| Trait | Ableiten? | Was du bekommst |
|---|---|---|
Debug |
Fast immer | Ausgabe mit {:?} und {:#?} |
Clone |
Meistens | .clone() |
Copy |
Nur für kleine Typen ohne Felder vom Typ String, Vec oder Box |
Automatische Kopien statt Moves |
PartialEq |
Meistens | == und != |
Eq |
Wenn es keine Gleitkomma-Felder gibt | Nötig für Schlüssel in einer HashMap (zusammen mit Hash) |
Hash |
Wenn der Typ als Schlüssel in einer HashMap oder in einem HashSet dient |
Hashing |
PartialOrd, Ord |
Wenn die Werte eine natürliche Reihenfolge haben | <, >, .sort(), .max() |
Default |
Wenn ein „leerer“ Wert sinnvoll ist | ::default() und ..Default::default() |
Display |
Nicht ableitbar – von Hand schreiben | Ausgabe mit {} und .to_string() |
Eine sehr häufige erste Zeile ist #[derive(Debug, Clone, PartialEq)]. Füg weitere hinzu, wenn du sie brauchst – der Compiler sagt dir Bescheid, wenn einer fehlt.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“Copy für ein Struct mit einem String ableiten.
#[derive(Clone, Copy)]struct User { name: String, age: u32,}error[E0204]: the trait `Copy` cannot be implemented for this type --> src/main.rs:2:8 |1 | #[derive(Clone, Copy)] | ---- in this derive macro expansion2 | struct User { | ^^^^3 | name: String, | ------------ this field does not implement `Copy`Der Compiler sagt: Der Trait Copy kann für diesen Typ nicht implementiert werden, weil das Feld name kein Copy implementiert. Ein String besitzt Speicher auf dem Heap und lässt sich deshalb nicht Bit für Bit kopieren. Entferne Copy und benutze .clone(), wenn du ein Duplikat brauchst.
Eq für ein Struct mit einer Gleitkommazahl ableiten.
#[derive(Debug, PartialEq, Eq)]struct Product { name: String, price: f64 }error[E0277]: the trait bound `f64: Eq` is not satisfied --> src/main.rs:2:32 |1 | #[derive(Debug, PartialEq, Eq)] | -- in this derive macro expansion2 | struct Product { name: String, price: f64 } | ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`Hier meldet der Compiler, dass f64 den Trait Eq nicht implementiert. Behalte PartialEq und lass Eq weg. Oder speichere den Preis als ganze Cent in einem u64 – bei Geld ist das ohnehin sicherer.
Übungen
Abschnitt betitelt „Übungen“1. Eine To-do-Liste sortieren
Abschnitt betitelt „1. Eine To-do-Liste sortieren“Leg ein Struct Task mit priority: u8 und title: String an. Leite die Traits ab, die du brauchst, um einen Vec<Task> zu sortieren, jede Aufgabe mit {:?} auszugeben und zwei Aufgaben mit == zu vergleichen. Kleinere Zahlen sind dringender und sollen deshalb zuerst kommen. Tipp: .sort() braucht Ord, und Ord braucht auch Eq, PartialEq und PartialOrd. Schreib priority als erstes Feld, damit es zuerst verglichen wird.
Lösung anzeigen
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]struct Task { priority: u8, title: String,}
fn main() { let mut tasks = vec![ Task { priority: 3, title: String::from("Pflanzen gießen") }, Task { priority: 1, title: String::from("Miete zahlen") }, Task { priority: 2, title: String::from("Sam antworten") }, ];
let copy = tasks[1].clone(); println!("Dieselbe Aufgabe? {}", copy == tasks[1]);
tasks.sort(); for task in &tasks { println!("{:?}", task); }}Ausgabe:
Dieselbe Aufgabe? trueTask { priority: 1, title: "Miete zahlen" }Task { priority: 2, title: "Sam antworten" }Task { priority: 3, title: "Pflanzen gießen" }2. Geld, das sich ausgeben und addieren lässt
Abschnitt betitelt „2. Geld, das sich ausgeben und addieren lässt“Leg ein Struct Money an, das ganze Cent als cents: u64 speichert. Implementiere From<u64>, damit 350.into() funktioniert, Display, damit der Betrag wie 3,50 € ausgegeben wird, und Add, damit du zwei Beträge mit + addieren kannst. Tipp: Die Euro sind cents / 100, der Rest ist cents % 100. Gib den Rest mit {:02} aus, damit 5 Cent als 05 erscheinen. Leite Copy ab, damit du die Beträge nach dem Addieren noch ausgeben kannst.
Lösung anzeigen
use std::fmt;use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Money { cents: u64,}
impl From<u64> for Money { fn from(cents: u64) -> Self { Money { cents } }}
impl fmt::Display for Money { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{},{:02} €", self.cents / 100, self.cents % 100) }}
impl Add for Money { type Output = Money;
fn add(self, other: Money) -> Money { Money { cents: self.cents + other.cents } }}
fn main() { let coffee: Money = 350.into(); let cake = Money::from(405); let total = coffee + cake;
println!("Kaffee: {coffee}"); println!("Kuchen: {cake}"); println!("Gesamt: {total}");}Ausgabe:
Kaffee: 3,50 €Kuchen: 4,05 €Gesamt: 7,55 €Zusammenfassung
Abschnitt betitelt „Zusammenfassung“Debug({:?}) ist für Entwickler und wird fast immer abgeleitet.Display({}) ist für Benutzer und wird von Hand geschrieben.Clonekopiert auf Anfrage mit.clone().Copykopiert automatisch, aber nur bei einfachen Typen.PartialEq/Eqgeben dir==.PartialOrd/Ordgeben dir<und Sortieren und vergleichen die Felder von oben nach unten.Defaulterzeugt einen Startwert;..Default::default()füllt die Felder auf, die du nicht gesetzt hast.- Implementierst du
From, bekommst duIntogeschenkt. Der?-Operator benutztFrom, um Fehler umzuwandeln. - Implementiere
std::ops::Add(und seine Verwandten), damit+mit deinen eigenen Typen funktioniert.
Du willst tiefer einsteigen? Fortgeschrittene Traits erklärt assoziierte Typen wie type Output, Standard-Typparameter und wie du einen Trait für einen Typ implementierst, den du nicht selbst geschrieben hast.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Welchen Trait benutzt {} in println!?
2. Warum kann ein Struct mit einem String-Feld Copy nicht ableiten?
3. Wie werden zwei Structs verglichen, wenn du PartialOrd ableitest?
4. Du hast From<ParseIntError> for AppError implementiert. Was kann ? dadurch?
5. Welchen Trait implementierst du, damit a + b mit deinem eigenen Typ funktioniert?
Punkte: 0 / 5
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Anhang C: Ableitbare Traits – Das Rust-Buch: alle Traits, die du mit
#[derive]ableiten kannst, was die abgeleitete Version tut und wann du sie brauchst. std::fmt– Formatierung und Display (auf Englisch): alle Formatierungsoptionen wie Breite, Auffüllen und{:#?}, und wieDisplayundDebugdazu passen.FromundInto– Rust by Example (auf Englisch): kurze, ausführbare Beispiele für Umwandlungen zwischen deinen eigenen Typen.std::ops– Traits für Operatoren (auf Englisch): die vollständige Liste der Operatoren, die du überladen kannst – vonAddbisIndex.std::cmp– Traits für Vergleiche (auf Englisch):PartialEq,Eq,PartialOrdundOrdim Detail, mit den Regeln, die jeder von ihnen verspricht.