Zum Inhalt springen

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.

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: 44
mit try_from: Err(TryFromIntError(PosOverflow))
mit from: 300
▶ Selbst ausprobieren

Diese 44 ist genau die Art von Fehler, die sich monatelang versteckt. Die Konvertierungs-Traits lösen das auf drei Arten:

  • Sie sind ehrlich. From ist nur implementiert, wenn nichts schiefgehen kann. Kann etwas schiefgehen, nimmst du TryFrom. Das gibt ein Result zurück.
  • Sie funktionieren mit deinen eigenen Typen. (255, 0, 0) as Color kannst du nicht schreiben, aber du kannst From<(u8, u8, u8)> for Color implementieren.
  • 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 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 -> String
let big = i64::from(42_i32); // i32 -> i64, passt immer
let precise = f64::from(7_u8); // u8 -> f64, immer exakt
let 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.

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=235
▶ Selbst ausprobieren

Das 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 eine Color aus 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)
▶ Selbst ausprobieren

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.

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))
▶ Selbst ausprobieren

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.

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.

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)
▶ Selbst ausprobieren

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(&note)); // &String
println!("{}", word_count(note)); // String
}

Ausgabe:

3
4
4
▶ Selbst ausprobieren

In der Funktion liefert dir text.as_ref() ein ganz normales &str, egal was der Aufrufer übergeben hat.

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:

txt
jpeg
gz
(keine)
▶ Selbst ausprobieren

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.

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().

.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 this
13 | 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` trait

Die 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.)

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 Uhrzeit
Mittag -> Fehler: kein ':' gefunden
▶ Selbst ausprobieren

Das ? 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.

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:30
Timer gestellt auf 1:05
▶ Selbst ausprobieren

Wir 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.

  • as ist 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 From für Konvertierungen, die immer klappen, und du bekommst Into geschenkt. .into() muss seinen Zieltyp kennen.
  • Der ?-Operator ruft From auf, um Fehlertypen umzuwandeln. Mit impl From<OtherError> for MyError bleibt die Fehlerbehandlung kurz.
  • Nimm TryFrom / TryInto, wenn eine Konvertierung schiefgehen kann. Sie geben ein Result zurück.
  • Implementiere FromStr, damit .parse::<DeinTyp>() mit Text funktioniert.
  • Nimm impl AsRef<str> oder impl AsRef<Path>, um viele Eingabetypen billig und ohne Kopieren anzunehmen.

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