Konvertierungen in Rust: From, Into, TryFrom, FromStr und AsRef
Programme wandeln ständig Dinge um. Text aus einem Formular wird zu einer Zahl. Ein Tupel aus drei Zahlen wird zu einer Color. Ein Fehlertyp wird zu einem anderen. Rust hat dafür eine kleine Familie von Standard-Traits. Sobald du sie kennst, siehst du sie in echtem Code überall.
Stell dir diese Traits wie die Reiseadapter vor, die du für einen Urlaub im Ausland einpackst. Jeder ist für eine bestimmte Aufgabe gemacht. Manche passen immer (From). Manche passen vielleicht nicht und sagen dir das auch (TryFrom). Manche lesen ein Etikett und bauen daraus das passende Ding (FromStr). Und manche lassen dich einfach einstecken, was du gerade hast (AsRef). Diese Seite geht sie alle in Ruhe durch. as kennst du schon von der Seite Typumwandlung, und einen ersten Blick auf From hast du auf der Seite Wichtige Traits geworfen. Darauf bauen wir auf.
Warum nicht einfach as?
Abschnitt betitelt „Warum nicht einfach as?“as ist schnell, hat aber zwei Grenzen. Es funktioniert nur zwischen einfachen eingebauten Typen wie Zahlen, char und bool. Und es schlägt nie fehl: Passt ein Wert nicht, bekommst du stillschweigend eine andere Zahl.
fn main() { let items_in_cart: i32 = 300;
// `as` quetscht 300 in einen u8 und liegt stillschweigend daneben let squeezed = items_in_cart as u8; println!("mit as: {squeezed}");
// `try_from` bemerkt das Problem und sagt es dir let checked = u8::try_from(items_in_cart); println!("mit try_from: {:?}", checked);
// `from` gibt es nur, wenn die Konvertierung nie Daten verlieren kann let widened = i64::from(items_in_cart); println!("mit from: {widened}");}Ausgabe:
mit as: 44mit try_from: Err(TryFromIntError(PosOverflow))mit from: 300Diese 44 ist genau die Art von Fehler, die sich monatelang versteckt. Die Konvertierungs-Traits lösen das auf drei Arten:
- Sie sind ehrlich.
Fromist nur implementiert, wenn nichts schiefgehen kann. Kann etwas schiefgehen, nimmst duTryFrom. Das gibt einResultzurück. - Sie funktionieren mit deinen eigenen Typen.
(255, 0, 0) as Colorkannst du nicht schreiben, aber du kannstFrom<(u8, u8, u8)> for Colorimplementieren. - Anderer Code versteht sie. Der
?-Operator,.parse()und viele Bibliotheksfunktionen bauen auf diesen Traits auf.
Eine gute Faustregel lautet also: Nimm as für schnelle, sichere Zahlentricks und greif überall sonst zu den Traits.
From und Into: Konvertierungen, die immer klappen
Abschnitt betitelt „From und Into: Konvertierungen, die immer klappen“From sagt: „Ich weiß, wie ich mich aus diesem anderen Typ baue.“ Du hast es schon benutzt. Viele Standardtypen implementieren es:
let greeting = String::from("hallo"); // &str -> Stringlet big = i64::from(42_i32); // i32 -> i64, passt immerlet precise = f64::from(7_u8); // u8 -> f64, immer exaktlet scores = Vec::from([90, 85, 77]); // Array -> Vec<i32>Achte darauf, was fehlt: Es gibt kein i32::from(some_i64). Ein i64 passt vielleicht nicht in einen i32, also tut Rust nicht so, als würde das immer klappen. Darauf kommen wir im Abschnitt über TryFrom zurück.
From für deinen eigenen Typ implementieren
Abschnitt betitelt „From für deinen eigenen Typ implementieren“Hier lässt sich eine Color aus einem Tupel mit Rot-, Grün- und Blauwert bauen:
#[derive(Debug)]struct Color { red: u8, green: u8, blue: u8,}
impl From<(u8, u8, u8)> for Color { fn from((red, green, blue): (u8, u8, u8)) -> Self { Color { red, green, blue } }}
fn main() { let orange = Color::from((255, 136, 0)); println!("{:?}", orange);
// `Into` haben wir nie geschrieben, aber wir bekommen es geschenkt: let sky: Color = (135, 206, 235).into(); println!("Himmelblau ist r={} g={} b={}", sky.red, sky.green, sky.blue);}Ausgabe:
Color { red: 255, green: 136, blue: 0 }Himmelblau ist r=135 g=206 b=235Das ist das Schöne daran. Die Standardbibliothek sagt: „Wenn B den Trait From<A> implementiert, dann implementiert A automatisch Into<B>.“ Ein einziges impl From gibt dir also zwei Wege zum Umwandeln:
Color::from(tuple)liest sich wie „bau eineColoraus diesem Wert“.tuple.into()liest sich wie „mach daraus, was hier gebraucht wird“.
Warum haben wir let sky: Color geschrieben? Weil .into() nicht sagt, wohinein umgewandelt werden soll. Ein Tupel könnte theoretisch in viele Typen umgewandelt werden. Die Typangabe sagt Rust, welchen du meinst. Kennt Rust den Zieltyp schon, zum Beispiel von einem Funktionsparameter, brauchst du sie nicht.
Du siehst .into() auch oft bei Funktionsparametern. Eine Funktion wie fn new(title: impl Into<String>) nimmt sowohl ein &str als auch einen String an und ruft intern title.into() auf. Dort kennt Rust das Ziel, also ist keine Typangabe nötig. Dieses Muster probierst du in Übung 2 selbst aus.
From in der Fehlerbehandlung: wie ? Fehler umwandelt
Abschnitt betitelt „From in der Fehlerbehandlung: wie ? Fehler umwandelt“Hier ist eine Stelle, an der From still und leise viel Arbeit für dich erledigt. Trifft ? auf ein Err, gibt es den Fehler nicht einfach zurück. Zuerst ruft es From::from auf dem Fehler auf, um ihn in den Fehlertyp umzuwandeln, den deine Funktion zurückgibt.
Gibt deine Funktion also deinen eigenen AppError zurück, und AppError implementiert From<ParseIntError>, dann kann ? einen Parse-Fehler ganz von selbst in einen AppError verwandeln:
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { MissingEquals, BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
// Liest eine Konfigurationszeile wie "max_users = 50"fn read_setting(line: &str) -> Result<(String, u32), AppError> { let (key, value) = line.split_once('=').ok_or(AppError::MissingEquals)?; let number = value.trim().parse::<u32>()?; // ParseIntError -> AppError Ok((key.trim().to_string(), number))}
fn main() { println!("{:?}", read_setting("max_users = 50")); println!("{:?}", read_setting("max_users = viele")); println!("{:?}", read_setting("max_users 50"));}Ausgabe:
Ok(("max_users", 50))Err(BadNumber(ParseIntError { kind: InvalidDigit }))Err(MissingEquals)Ohne das impl From würde das zweite ? nicht kompilieren, weil Rust nicht wüsste, wie aus einem ParseIntError ein AppError wird. Mit ihm bleibt dein Funktionsrumpf schön aufgeräumt. Die Seite Eigene Fehlertypen geht tiefer darauf ein, wie du Fehlertypen entwirfst. Und die Seite anyhow und thiserror zeigt Crates, die dir diese From-Implementierungen abnehmen.
TryFrom und TryInto: Konvertierungen, die schiefgehen können
Abschnitt betitelt „TryFrom und TryInto: Konvertierungen, die schiefgehen können“Manche Konvertierungen klappen nur manchmal. 300 passt nicht in einen u8. -5 ist kein gültiger Prozentwert. Für solche Fälle hat Rust TryFrom. Es funktioniert wie From, gibt aber ein Result zurück. Du musst dich also um den „Nein“-Fall kümmern. Das ist gut so: Der Fehler kann sich nicht an dir vorbeischleichen.
Die eingebaute Variante hast du oben auf dieser Seite schon gesehen: u8::try_from(300_i32) gibt ein Err zurück statt einer falschen Zahl. Und so wie zu From der Trait Into gehört, gehört zu TryFrom der Trait TryInto. let small: Result<u8, _> = big.try_into(); funktioniert also auch. TryInto ist seit der Edition 2021 im Prelude, du musst also nichts importieren.
TryFrom für deinen eigenen Typ implementieren
Abschnitt betitelt „TryFrom für deinen eigenen Typ implementieren“Bauen wir einen Typ Percentage, der nur Werte von 0 bis 100 enthalten kann. Wird jeder Percentage-Wert über try_from erzeugt, dann ist jeder Percentage-Wert in deinem Programm gültig. Du prüfst einmal, gleich an der Tür, und nie wieder.
#[derive(Debug)]struct Percentage(u8);
#[derive(Debug)]struct OutOfRange(i32);
impl TryFrom<i32> for Percentage { type Error = OutOfRange;
fn try_from(value: i32) -> Result<Self, Self::Error> { if (0..=100).contains(&value) { Ok(Percentage(value as u8)) // sicher: wir haben den Bereich gerade geprüft } else { Err(OutOfRange(value)) } }}
fn main() { println!("{:?}", Percentage::try_from(85)); println!("{:?}", Percentage::try_from(150));
let battery: Result<Percentage, _> = (-5).try_into(); println!("{:?}", battery);}Ausgabe:
Ok(Percentage(85))Err(OutOfRange(150))Err(OutOfRange(-5))Die Zeile type Error = OutOfRange; sagt Rust, welche Art von Fehler diese Konvertierung zurückgibt. Den wählst du selbst. Hier trägt er den ungültigen Wert mit sich. So kann jeder, der den Fehler bekommt, eine hilfreiche Meldung anzeigen.
FromStr und .parse(): Werte aus Text bauen
Abschnitt betitelt „FromStr und .parse(): Werte aus Text bauen“Text ist das, was du am häufigsten umwandeln wirst. Dateien, Kommandozeilenargumente und Webformulare liefern dir alle Zeichenketten. Der Trait FromStr beschreibt, „wie man diesen Typ aus einem &str baut“, und .parse() ist der bequeme Weg, es aufzurufen.
Schreibst du "42".parse::<i32>(), sucht Rust die FromStr-Implementierung von i32 und führt sie aus. Der Teil ::<i32> (man nennt ihn „Turbofish“) sagt Rust, welchen Typ du willst. Eine Typangabe an der Variablen funktioniert genauso.
let age = "42".parse::<i32>(); // Ok(42)let price: Result<f64, _> = "4.99".parse(); // Ok(4.99)let broken = "zweiundvierzig".parse::<i32>(); // Err(ParseIntError { kind: InvalidDigit })Jeder Aufruf gibt ein Result zurück, denn Text kann immer falsch sein.
FromStr für deinen eigenen Typ implementieren
Abschnitt betitelt „FromStr für deinen eigenen Typ implementieren“Stell dir einen Bilderrahmen-Laden vor, in dem Kunden Größen wie "30x40" eintippen. Bringen wir Rust bei, das zu lesen:
use std::str::FromStr;
#[derive(Debug)]struct Size { width: u32, height: u32,}
#[derive(Debug)]enum SizeError { MissingX, BadNumber,}
impl FromStr for Size { type Err = SizeError;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (w, h) = text.split_once('x').ok_or(SizeError::MissingX)?; let width = w.trim().parse().map_err(|_| SizeError::BadNumber)?; let height = h.trim().parse().map_err(|_| SizeError::BadNumber)?; Ok(Size { width, height }) }}
fn main() { println!("{:?}", "30x40".parse::<Size>());
println!("{:?}", "30 mal 40".parse::<Size>()); println!("{:?}", "30xgroß".parse::<Size>());}Ausgabe:
Ok(Size { width: 30, height: 40 })Err(MissingX)Err(BadNumber)Zwei kleine Details. Erstens heißt der Fehlertyp bei FromStr Err, nicht Error wie bei TryFrom. Das ist eine alte Eigenheit bei der Benennung. Keine Sorge, wenn du die beiden anfangs verwechselst: Der Compiler erinnert dich daran. Zweitens brauchst du use std::str::FromStr;, um den Trait zu implementieren, aber nicht, um .parse() aufzurufen.
.map_err(|_| SizeError::BadNumber) tauscht den Fehler beim Zahlen-Parsen gegen unseren eigenen Fehler aus. Das ist eine praktische Alternative zu einer From-Implementierung, wenn du die Umwandlung nur an einer Stelle brauchst.
AsRef: alles annehmen, was dir eine Ansicht leihen kann
Abschnitt betitelt „AsRef: alles annehmen, was dir eine Ansicht leihen kann“Die bisherigen Traits erzeugen einen neuen Wert. AsRef ist anders. Es baut gar nichts. Es sagt nur: „Ich kann dir eine billige, ausgeliehene Ansicht von mir als diesen Typ geben.“
Wozu soll das gut sein? Angenommen, du schreibst eine Funktion, die Wörter zählt. Nimmt sie ein &str, muss jemand mit einem String &name schreiben. Nimmt sie einen String, muss jemand mit einem &str erst einen neuen String anlegen. Mit impl AsRef<str> funktioniert einfach alles:
fn word_count(text: impl AsRef<str>) -> usize { text.as_ref().split_whitespace().count()}
fn main() { let note = String::from("Milch und Eier kaufen");
println!("{}", word_count("hallo Rust Welt")); // &str println!("{}", word_count(¬e)); // &String println!("{}", word_count(note)); // String}Ausgabe:
344In der Funktion liefert dir text.as_ref() ein ganz normales &str, egal was der Aufrufer übergeben hat.
AsRef in der Standardbibliothek: Pfade
Abschnitt betitelt „AsRef in der Standardbibliothek: Pfade“Wahrscheinlich hast du AsRef schon benutzt, ohne es zu merken. Die Dateifunktionen in std::fs sehen so aus:
pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>Deshalb funktionieren fs::read_to_string("notizen.txt"), fs::read_to_string(my_string) und fs::read_to_string(my_path_buf) alle. Denselben Trick kannst du in deinem eigenen Code verwenden. Dieses Beispiel schaut sich nur Pfadnamen an, es fasst also keine echten Dateien an:
use std::path::{Path, PathBuf};
fn extension_of(path: impl AsRef<Path>) -> String { match path.as_ref().extension() { Some(ext) => ext.to_string_lossy().to_string(), None => String::from("(keine)"), }}
fn main() { println!("{}", extension_of("notizen.txt")); println!("{}", extension_of(String::from("urlaub.jpeg"))); println!("{}", extension_of(PathBuf::from("backups/2024/site.tar.gz"))); println!("{}", extension_of("README"));}Ausgabe:
txtjpeggz(keine)AsMut ist dieselbe Idee, leiht aber eine veränderbare Ansicht. Zum Beispiel können sich sowohl ein Vec als auch ein Array als veränderbarer Slice ausleihen:
fn reset_all(scores: &mut impl AsMut<[u32]>) { for score in scores.as_mut() { *score = 0; }}
// Beides funktioniert:// reset_all(&mut vec![12, 7, 30]);// reset_all(&mut [3, 1, 4]);AsMut brauchst du viel seltener als AsRef. Es reicht, wenn du es erkennst, sobald es dir begegnet.
Welches nehme ich?
Abschnitt betitelt „Welches nehme ich?“Wenn du dir unsicher bist, ist diese Tabelle ein guter Startpunkt:
| Du willst … | Nimm | Beispiel |
|---|---|---|
| Schnell zwischen Zahlentypen umwandeln und Abschneiden in Kauf nehmen | as |
x as f64 |
| Umwandeln, wenn es nie schiefgehen kann | From |
i64::from(n), String::from("hi") |
| Dasselbe, wenn der Zieltyp schon bekannt ist | Into |
let s: String = "hi".into(); |
| Umwandeln, wenn es schiefgehen kann | TryFrom / TryInto |
u8::try_from(300) |
| Einen Wert aus Text bauen | FromStr / .parse() |
"42".parse::<i32>() |
| Viele string- oder pfadartige Eingaben billig annehmen | AsRef |
fn f(p: impl AsRef<Path>) |
| Einen Wert in lesbaren Text verwandeln | Display → .to_string() |
42.to_string() |
Die letzte Zeile bekommst du geschenkt: Implementiere Display (siehe Wichtige Traits), und jeder Wert deines Typs hat eine Methode .to_string().
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“.into() aufrufen, wenn Rust den Zieltyp nicht erkennen kann.
fn main() { let name = "Ferris".into(); println!("{name}");}error[E0283]: type annotations needed --> src/main.rs:2:9 |2 | let name = "Ferris".into(); | ^^^^ ---- type must be known at this point | = note: the type must implement `From<&str>` = note: required for `&str` to implement `Into<_>`help: consider giving `name` an explicit type |2 | let name: /* Type */ = "Ferris".into(); | ++++++++++++Der Compiler braucht eine Typangabe („type annotations needed“), weil der Typ an dieser Stelle bekannt sein muss. Ein &str kann zu einem String, einer Box<str>, einem Vec<u8> und mehr werden. Rust rät nicht. Gib den Typ an: let name: String = "Ferris".into();, oder schreib einfach String::from("Ferris").
Into statt From implementieren. Es sieht so aus, als müsste das dasselbe sein. Deshalb tappt man hier leicht in die Falle:
use std::num::ParseIntError;
#[derive(Debug)]struct AppError(String);
impl Into<AppError> for ParseIntError { fn into(self) -> AppError { AppError(self.to_string()) }}
fn read_port(text: &str) -> Result<u16, AppError> { let port = text.parse::<u16>()?; Ok(port)}error[E0277]: `?` couldn't convert the error to `AppError` --> src/main.rs:13:35 |12 | fn read_port(text: &str) -> Result<u16, AppError> { | --------------------- expected `AppError` because of this13 | let port = text.parse::<u16>()?; | --------------^ the trait `From<ParseIntError>` is not implemented for `AppError` | = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` traitDie Meldung sagt: ? konnte den Fehler nicht in einen AppError umwandeln, weil From<ParseIntError> für AppError nicht implementiert ist. Der Hinweis darunter erklärt, dass ? die Umwandlung immer über den Trait From macht.
Das Geschenk gibt es nämlich nur in eine Richtung: Wer From implementiert, bekommt Into dazu. Wer Into implementiert, bekommt kein From. Und ? verlangt, genau wie viel Bibliothekscode, nach From. Implementiere also immer From und lass Into von selbst mitkommen. (Clippy, der Linter von Rust, warnt sogar davor.)
Übungen
Abschnitt betitelt „Übungen“1. Eine Uhrzeit einlesen
Abschnitt betitelt „1. Eine Uhrzeit einlesen“Bau ein Struct Time mit hour: u8 und minute: u8. Implementiere FromStr, damit "09:45".parse::<Time>() funktioniert. Gib einen Fehler zurück, wenn kein : vorkommt, wenn ein Teil keine Zahl ist oder wenn die Stunde über 23 bzw. die Minute über 59 liegt. Probier es mit "09:45", "25:00" und "Mittag" aus. Tipp: split_once(':') liefert dir die beiden Hälften. Als Fehlertyp reicht ein einfacher String.
Lösung anzeigen
use std::str::FromStr;
#[derive(Debug)]struct Time { hour: u8, minute: u8,}
impl FromStr for Time { type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (h, m) = text.split_once(':').ok_or("kein ':' gefunden")?; let hour: u8 = h.parse().map_err(|_| "Stunde ist keine Zahl")?; let minute: u8 = m.parse().map_err(|_| "Minute ist keine Zahl")?;
if hour > 23 || minute > 59 { return Err(format!("{text} ist keine gültige Uhrzeit")); } Ok(Time { hour, minute }) }}
fn main() { for input in ["09:45", "25:00", "Mittag"] { match input.parse::<Time>() { Ok(t) => println!("{input} -> {} Std. {} Min.", t.hour, t.minute), Err(e) => println!("{input} -> Fehler: {e}"), } }}Ausgabe:
09:45 -> 9 Std. 45 Min.25:00 -> Fehler: 25:00 ist keine gültige UhrzeitMittag -> Fehler: kein ':' gefundenDas ? nach .ok_or("kein ':' gefunden") funktioniert, weil String den Trait From<&str> implementiert. Das ist derselbe From-Trick wie im Abschnitt über Fehlerbehandlung, diesmal mit einem Standardtyp.
2. Ein Küchentimer
Abschnitt betitelt „2. Ein Küchentimer“Bau ein Struct Timer mit minutes: u32 und seconds: u32. Implementiere From<u32>, damit aus einer Gesamtzahl von Sekunden ein Timer wird (aus 150 werden 2 Minuten 30 Sekunden). Schreib dann fn start(timer: impl Into<Timer>), das die Zeit ausgibt, und ruf es mit 150 und mit Timer { minutes: 1, seconds: 5 } auf. Tipp: Die Minuten sind total / 60, der Rest ist total % 60. Jeder Typ lässt sich schon in sich selbst umwandeln, also funktioniert auch die Übergabe eines Timer.
Lösung anzeigen
struct Timer { minutes: u32, seconds: u32,}
impl From<u32> for Timer { fn from(total: u32) -> Self { Timer { minutes: total / 60, seconds: total % 60 } }}
fn start(timer: impl Into<Timer>) { let t = timer.into(); println!("Timer gestellt auf {}:{:02}", t.minutes, t.seconds);}
fn main() { start(150_u32); start(Timer { minutes: 1, seconds: 5 });}Ausgabe:
Timer gestellt auf 2:30Timer gestellt auf 1:05Wir haben nur From<u32> geschrieben, und trotzdem nimmt start sowohl eine Zahl als auch einen Timer an. Die Zahl funktioniert über unser From, der Timer, weil die Standardbibliothek festlegt, dass sich jeder Typ in sich selbst umwandeln lässt.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“asist für schnelle Zahlen-Casts gedacht und kann stillschweigend Daten verlieren. Die Konvertierungs-Traits sind ausdrücklich, sicher und funktionieren mit deinen eigenen Typen.- Implementiere
Fromfür Konvertierungen, die immer klappen, und du bekommstIntogeschenkt..into()muss seinen Zieltyp kennen. - Der
?-Operator ruftFromauf, um Fehlertypen umzuwandeln. Mitimpl From<OtherError> for MyErrorbleibt die Fehlerbehandlung kurz. - Nimm
TryFrom/TryInto, wenn eine Konvertierung schiefgehen kann. Sie geben einResultzurück. - Implementiere
FromStr, damit.parse::<DeinTyp>()mit Text funktioniert. - Nimm
impl AsRef<str>oderimpl AsRef<Path>, um viele Eingabetypen billig und ohne Kopieren anzunehmen.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Du implementierst From<Celsius> for Fahrenheit. Was bekommst du automatisch dazu?
2. Mit welchem Aufruf wandelst du einen i32, der auch 300 sein könnte, sicher in einen u8 um?
3. Was musst du implementieren, damit "30x40".parse::<Size>() funktioniert?
4. Warum kompiliert let name = "Ferris".into(); nicht?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“std::convert– Doku der Standardbibliothek (auf Englisch): das Zuhause vonFrom,Into,TryFrom,TryInto,AsRefundAsMut, mit einer kurzen Anleitung, wann du welches implementierst.FromStr– Doku der Standardbibliothek (auf Englisch): der Trait hinter.parse(), mit einem vollständigen Implementierungsbeispiel.AsRef– Doku der Standardbibliothek (auf Englisch): wie sichAsRefvonBorrowunterscheidet und warum es für billige Referenz-Konvertierungen verwendet wird.- Conversion – Rust by Example (auf Englisch): kurze, ausführbare Kapitel zu
From/Into,TryFrom/TryIntound zur Umwandlung von und in Strings. - Conversions – Rust API Guidelines (auf Englisch): die Empfehlungen der Community, welche Konvertierungs-Traits deine eigenen Typen implementieren sollten.