Option<T> ist Rusts Antwort auf das Null-Problem. In Sprachen wie Java, C, JavaScript ist jede Referenz potenziell null — und vergessenes Null-Check ist eine der häufigsten Bug-Quellen überhaupt (Tony Hoare nannte es seinen „Billion-Dollar-Mistake"). Rust eliminiert das Problem komplett: ein Wert ist entweder Some(T) (vorhanden) oder None (nicht vorhanden). Der Compiler verlangt, dass du beide Fälle behandelst, bevor du auf den Wert zugreifst. Dieser Artikel zeigt das vollständige API von Option, die wichtigsten Patterns und macht klar, warum diese eine Typ-Idee viele klassische Bugs eliminiert.
Hinweis:
Option<T>ist ein generischer Typ —<T>steht für „irgendein Inhalts-Typ". Generics als Mechanismus werden im Generics-Kapitel ausführlich erklärt; hier nutzen wirOptioneinfach als fertigen Stdlib-Typ und vertrauen darauf, dassOption<i32>,Option<String>etc. für den jeweiligen Inhaltstyp das tun, was man erwartet.
Definition
Option<T> ist ein einfaches Stdlib-Enum:
// Aus der Stdlib (vereinfacht):
pub enum Option<T> {
None,
Some(T),
}Beide Varianten sind so wichtig, dass sie im Prelude sind — du kannst Some(...) und None ohne Option::-Präfix nutzen.
fn main() {
let a: Option<i32> = Some(42);
let b: Option<i32> = None;
println!("{a:?} {b:?}"); // Some(42) None
}Niche-Optimization
Eine besondere Eigenheit: für viele Typen ist Option<T> genauso groß wie T selbst:
use std::mem::size_of;
fn main() {
println!("{}", size_of::<&i32>()); // 8
println!("{}", size_of::<Option<&i32>>()); // 8 — gleich!
println!("{}", size_of::<Box<i32>>()); // 8
println!("{}", size_of::<Option<Box<i32>>>()); // 8 — gleich!
}&T und Box<T> haben den Null-Pointer nicht als legalen Wert. Der Compiler nutzt das 0x0...0-Pattern als None-Marker. Damit ist Option<&T> syntaktisch typsicher und im Speicher identisch zu einem nullable Pointer in C.
Pattern-Matching auf Option
fn beschreibe(o: Option<i32>) -> String {
match o {
Some(n) => format!("Wert: {n}"),
None => "kein Wert".to_string(),
}
}
fn main() {
println!("{}", beschreibe(Some(42))); // "Wert: 42"
println!("{}", beschreibe(None)); // "kein Wert"
}Klassisches match mit beiden Varianten. Der Compiler verlangt beide Branches.
if let — kompakter
fn drucke_wenn_da(o: Option<i32>) {
if let Some(n) = o {
println!("{n}");
}
// None-Fall wird ignoriert
}Wenn nur der Some-Fall interessiert: if let. Mehr im if-let-Artikel.
Das wichtigste API
unwrap — Wert oder Panic
fn main() {
let o = Some(42);
let n = o.unwrap(); // 42
// None.unwrap(); // Panic — „called Option::unwrap on a None value"
}unwrap() ist die einfachste Form, an den inneren Wert zu kommen: wenn Some(x) vorliegt, gibt sie x zurück; wenn None, panickt sie mit der Standard-Nachricht „called Option::unwrap on a None value".
Wann ist unwrap angemessen? In drei Situationen: in Tests, wo ein Panic eine ehrliche Aussage über einen Test-Bug ist; in Prototypen und Skripten, wo Robustheit egal ist und Schnelligkeit zählt; und in Code, wo None beweisbar unmöglich ist (etwa vec.first().unwrap() direkt nach einer len() > 0-Prüfung). In Production-Code, wo None ein erwarteter Zustand sein könnte, ist unwrap ein potentieller Crash und solltest durch sicherere Methoden ersetzt werden.
expect — Wert oder Panic mit Message
fn main() {
let o: Option<i32> = std::env::var("PORT").ok().and_then(|s| s.parse().ok());
let port = o.expect("PORT muss gesetzt sein und u32 sein");
println!("{port}");
}expect("nachricht") ist die idiomatischere Variante von unwrap, wenn du wirklich auf einen Wert bestehst. Statt einer generischen Fehler-Nachricht bekommst du eine selbst formulierte Begründung in der Panic-Ausgabe — was beim Debuggen Gold wert ist.
Die Konvention für die Nachricht: erkläre, warum None unmöglich sein sollte, nicht was schiefging. „PORT muss gesetzt sein und u32 sein" sagt zukünftigen Lesern: „der Entwickler hier hat die Annahme getroffen, dass PORT in der Umgebung definiert ist". Wenn das doch nicht stimmt, ist die Panic-Nachricht direkt der Bug-Report.
unwrap_or — Default
fn main() {
let o: Option<i32> = None;
let n = o.unwrap_or(0); // 0
assert_eq!(n, 0);
let o2 = Some(42);
let m = o2.unwrap_or(0); // 42
assert_eq!(m, 42);
}unwrap_or(default) ist die sichere Variante: bei Some(x) gibt sie x zurück, bei None den übergebenen Default. Kein Panic, kein Pattern-Match — eine einzige saubere Zeile.
Wichtig zu wissen: der Default-Wert wird sofort ausgewertet, egal ob die Option Some oder None ist. Bei billigen Defaults (Konstanten, primitive Werte) ist das egal; bei teuren Defaults (etwa unwrap_or(berechne_default())) wäre die Funktion bei jedem Aufruf ausgeführt — auch wenn das Ergebnis dann verworfen wird. Für teure Defaults gibt es die unwrap_or_else-Variante.
unwrap_or_else — lazy Default
fn main() {
let o: Option<String> = None;
let s = o.unwrap_or_else(|| {
// teure Berechnung — nur ausgeführt bei None
std::env::var("DEFAULT").unwrap_or_else(|_| "unbekannt".into())
});
println!("{s}");
}unwrap_or_else(closure) ist die lazy Variante: die Closure wird nur dann aufgerufen, wenn die Option tatsächlich None ist. Bei Some(x) passiert nichts mit der Closure — sie wird komplett übersprungen.
Das ist wichtig bei teuren Defaults: eine Environment-Variable lesen, eine Datei öffnen, einen API-Call machen, einen neuen Vec allozieren. Bei unwrap_or(berechne_teuer()) läuft die Berechnung immer; bei unwrap_or_else(|| berechne_teuer()) nur bei Bedarf. Performance-kritisch bei häufig aufgerufenen Funktionen.
Eine verwandte Variante ist unwrap_or_default(), die Default::default() als Fallback nutzt. Sie passt für Standard-Typen, die einen sinnvollen Default haben — String::default() gibt "", Vec::default() gibt einen leeren Vec, i32::default() gibt 0.
unwrap_or_default
fn main() {
let o: Option<String> = None;
let s = o.unwrap_or_default(); // ""
assert_eq!(s, "");
let o2: Option<i32> = None;
let n = o2.unwrap_or_default(); // 0
assert_eq!(n, 0);
}Default via Default-Trait. Funktioniert für alle Typen, die Default implementieren.
Transformationen
map — auf den Wert anwenden
fn main() {
let o = Some(5);
let m = o.map(|n| n * 2); // Some(10)
assert_eq!(m, Some(10));
let leer: Option<i32> = None;
let leer2 = leer.map(|n| n * 2); // None — Closure läuft nicht
assert_eq!(leer2, None);
}map ist die Standard-Operation für „transformiere den inneren Wert, falls vorhanden". Bei Some(x) wird die Closure auf x angewendet und das Ergebnis als Some(y) zurückgegeben. Bei None passiert nichts — die Closure läuft gar nicht, und das Ergebnis ist wieder None.
Damit kannst du Transformationen über eine ganze Kette führen, ohne in jedem Schritt auf None zu prüfen. o.map(f).map(g).map(h) läuft entweder vollständig durch (bei Some) oder bleibt bei None. Das ist eines der klassischen funktionalen Patterns aus der Welt von Haskell, ML und Scala — und in Rust völlig idiomatisch.
and_then (flat_map)
fn parse_zahl(s: &str) -> Option<i32> {
s.parse().ok()
}
fn verdopple(n: i32) -> Option<i32> {
if n < 0 { None } else { Some(n * 2) }
}
fn main() {
let r = Some("5").and_then(|s| parse_zahl(s)).and_then(verdopple);
assert_eq!(r, Some(10));
let r2 = Some("abc").and_then(|s| parse_zahl(s)).and_then(verdopple);
assert_eq!(r2, None);
}and_then ist der mächtige Bruder von map: er erlaubt verkettete fallible Operationen. Wo map eine Closure mit Rückgabe U nimmt (und in Option<U> wickelt), nimmt and_then eine Closure mit Rückgabe Option<U> und gibt direkt diese zurück — kein zusätzliches Wickeln.
Damit kannst du mehrere Operationen verketten, die jeweils scheitern können. Im Beispiel: erst parse_zahl (kann scheitern, wenn der String keine Zahl ist), dann verdopple (kann scheitern bei negativen Werten). Wenn irgendeine in der Kette None liefert, propagiert das automatisch durch — die folgenden Closures laufen nicht. Das Ergebnis ist eine flache Option<i32>, keine verschachtelte Option<Option<Option<i32>>>.
Dieses Pattern ist in funktionalen Sprachen unter dem Namen flatMap oder bind bekannt — es ist die Monad-Operation für die Option-Monade. Wer das mentale Modell hat, schreibt sehr klare, kompakte fallible Code-Ketten.
filter — bedingt auf None setzen
fn main() {
let o = Some(42);
let gerade = o.filter(|&n| n % 2 == 0); // Some(42)
let ungerade = Some(43).filter(|&n| n % 2 == 0); // None
assert_eq!(gerade, Some(42));
assert_eq!(ungerade, None);
}filter macht aus Some(v) ein None, wenn das Prädikat false liefert.
ok_or — zu Result konvertieren
fn main() {
let o: Option<i32> = Some(42);
let r: Result<i32, &str> = o.ok_or("kein Wert");
assert_eq!(r, Ok(42));
let n: Option<i32> = None;
let r2: Result<i32, &str> = n.ok_or("kein Wert");
assert_eq!(r2, Err("kein Wert"));
}ok_or ist der Brücken-Operator zwischen Option und Result. Aus Some(x) wird Ok(x), aus None wird Err(err) mit dem übergebenen Fehler-Wert. Sehr nützlich an Stellen, wo du eine optional-typisierte API in eine error-typisierte API überführen willst.
Das passiert oft an API-Grenzen: deine interne Logik arbeitet mit Option, deine öffentliche Funktion soll aber Result zurückgeben, weil Aufrufer einen aussagekräftigen Fehler erwarten. value.ok_or("nicht gefunden")? macht aus „optionaler Lookup" einen „Fehler mit Kontext" in einer Zeile, kombiniert mit dem ?-Operator. Analog gibt es ok_or_else(closure) für lazy Error-Konstruktion.
Der ?-Operator auf Option
Der ?-Operator funktioniert in Funktionen, die Option zurückgeben — bei None macht er ein early-return:
fn ersten_buchstaben(s: &str) -> Option<char> {
let erstes_wort = s.split_whitespace().next()?; // None → return None
erstes_wort.chars().next() // erstes Zeichen
}
fn main() {
assert_eq!(ersten_buchstaben("Hallo Welt"), Some('H'));
assert_eq!(ersten_buchstaben(""), None);
}Der ?-Operator ist eines der elegantesten Sprach-Features von Rust. In einer Funktion, die Option<T> zurückgibt, wandelt ? einen Option<U>-Ausdruck in U um — und macht bei None einen early return mit None.
Aus der match-basierten Variante mit fünf Zeilen pro Schritt wird eine einzige Zeile pro Schritt mit ?. Bei mehreren verketteten fallible Operationen reduziert das den Code-Umfang dramatisch. Vergleiche:
Ohne ?: let e = match s.split_whitespace().next() { Some(x) => x, None => return None };
Mit ?: let e = s.split_whitespace().next()?;
Der ?-Operator funktioniert sowohl auf Option als auch auf Result (mit etwas unterschiedlicher Semantik), und es gibt auch Konvertierung zwischen beiden über From. Im nächsten Artikel zu Result schauen wir das detailliert an.
Weitere wichtige Methoden
| Methode | Was sie tut |
|---|---|
is_some() | true wenn Some |
is_none() | true wenn None |
as_ref() | Option<T> → Option<&T> |
as_mut() | Option<T> → Option<&mut T> |
take() | Wert herausnehmen, None zurücklassen |
replace(x) | Wert ersetzen, alten zurückgeben |
or(other) | self wenn Some, sonst other |
or_else(f) | self wenn Some, sonst f() |
xor(other) | genau eines von beiden, oder None |
zip(other) | Option<(A, B)> aus zwei Options |
flatten() | Option<Option<T>> → Option<T> |
contains(&x) | true wenn Some(x) mit x == self.0 |
fn main() {
let s = Some(String::from("Hi"));
// Mit as_ref: nicht konsumieren
let laenge: Option<usize> = s.as_ref().map(|x| x.len());
// s ist hier noch nutzbar
println!("{s:?} hat Länge {laenge:?}");
}as_ref() ist die Konvertierungs-Methode, mit der du aus &Option<T> ein Option<&T> machst. Klingt unscheinbar, ist aber zentral: ohne as_ref würde s.map(|x| x.len()) den String s konsumieren — denn map nimmt den Wert per Move. Mit s.as_ref().map(|x| x.len()) arbeitest du nur auf einer Referenz, und der ursprüngliche Option<String> bleibt erhalten.
Dieses Pattern siehst du überall, wo Code mit Borrow-Semantik arbeitet — etwa bei &self-Methoden, die auf optionalen Feldern operieren wollen, ohne sie zu verbrauchen. Eine verwandte Methode ist as_mut() für mutable Referenzen.
take — Wert herausziehen
fn main() {
let mut o = Some(String::from("Hallo"));
let extrahiert = o.take(); // o ist jetzt None
assert_eq!(extrahiert, Some(String::from("Hallo")));
assert_eq!(o, None);
}take() ist eine elegante Lösung für ein häufiges Problem: du hast nur eine mutable Referenz auf eine Option und willst den inneren Wert herausnehmen. Direkter Move (o.unwrap()) ginge nicht, weil o eine Referenz ist. take() nimmt den Wert heraus und setzt das Original auf None.
Anwendungsfälle: In einer &mut self-Methode willst du ein Option-Feld konsumieren — self.feld.take() gibt dir den Inhalt, ohne self zu verbrauchen. Das ist die gleiche Mechanik wie std::mem::take für andere Typen, nur spezialisiert für Option mit None als Default.
Praxis: Option im echten Code
Optionale Config-Werte
pub struct AppConfig {
pub host: String,
pub port: u16,
pub log_datei: Option<String>, // optional
pub max_workers: Option<u32>, // optional
}
impl AppConfig {
pub fn workers(&self) -> u32 {
self.max_workers.unwrap_or(4) // Default 4
}
pub fn loggt_in_datei(&self) -> bool {
self.log_datei.is_some()
}
}Option<T> für optionale Felder. Default mit unwrap_or, Vorhandensein mit is_some.
Cache-Lookup
use std::collections::HashMap;
pub struct Cache { eintraege: HashMap<String, Vec<u8>> }
impl Cache {
pub fn holen(&self, key: &str) -> Option<&[u8]> {
self.eintraege.get(key).map(|v| v.as_slice())
}
}
fn main() {
let mut c = Cache { eintraege: HashMap::new() };
c.eintraege.insert("user:42".into(), vec![1, 2, 3]);
// Idiomatischer Zugriff mit if let
if let Some(daten) = c.holen("user:42") {
println!("Cache-Hit: {} Bytes", daten.len());
} else {
println!("Cache-Miss");
}
}Cache-Lookups sind ein typisches Beispiel, bei dem Option natürlich passt: das Element könnte im Cache sein oder nicht. HashMap::get gibt Option<&V> zurück — bei einem Treffer eine Referenz auf den Wert, bei einem Miss None. Du musst explizit auf beide Fälle reagieren.
Das .map(|v| v.as_slice()) konvertiert die Option<&Vec<u8>> in Option<&[u8]> — mehr Flexibilität für den Aufrufer, weil Slice-Methoden auch auf Arrays und Sub-Bereichen funktionieren. Im main siehst du dann das idiomatische Verbrauchs-Muster: if let Some(daten) = ... greift nur den Hit-Fall, der else-Zweig den Miss.
Funktion mit „kann fehlen"-Semantik
pub fn finde_user_by_email(email: &str, users: &[(u64, String)]) -> Option<u64> {
users.iter()
.find(|(_, mail)| mail == email)
.map(|(id, _)| *id)
}
fn main() {
let users = vec![
(1, "anna@example.com".to_string()),
(2, "bert@example.com".to_string()),
];
assert_eq!(finde_user_by_email("anna@example.com", &users), Some(1));
assert_eq!(finde_user_by_email("unbekannt@example.com", &users), None);
}Such-Funktionen geben naturgemäß Option zurück — entweder finden sie etwas oder nicht. Die find-Methode auf Iteratoren liefert Option<&Item>; mit .map(|x| ...) transformierst du das gefundene Element zu dem, was du eigentlich brauchst (hier die ID statt das ganze Tupel).
Diese find + map-Kombination ist überall in Rust-Code: „suche das passende Element, transformiere es zur gewünschten Repräsentation". Bei None (kein passendes Element gefunden) propagiert das automatisch durch — der Aufrufer bekommt None und muss den Nicht-Gefunden-Fall behandeln.
Chain von Operationen, jede kann fehlen
pub fn ersten_satz(text: &str) -> Option<&str> {
let trimmed = text.trim();
if trimmed.is_empty() {
return None;
}
trimmed.find('.').map(|i| &trimmed[..i])
}
fn main() {
assert_eq!(ersten_satz("Hallo. Welt."), Some("Hallo"));
assert_eq!(ersten_satz(""), None);
assert_eq!(ersten_satz("Keine Punkt"), None);
}Zwei Möglichkeiten zum frühen Ausstieg: is_empty → return None, dann find('.') mit map.
Iterator mit filter_map
fn main() {
let raw_zahlen = ["42", "abc", "100", "x", "7"];
let valide: Vec<i32> = raw_zahlen.iter()
.filter_map(|s| s.parse().ok())
.collect();
assert_eq!(valide, vec![42, 100, 7]);
}filter_map ist einer der elegantesten Iterator-Adapter überhaupt. Er kombiniert filter (Elemente verwerfen) und map (Elemente transformieren) in einer Operation: die Closure liefert Option<U>, und nur die Some(u)-Werte landen im Ergebnis.
Im Beispiel wird s.parse::<i32>() aufgerufen — das gibt Result<i32, ParseError> zurück. .ok() konvertiert das in Option<i32>: bei Erfolg Some(zahl), bei Fehler None. filter_map collect't dann nur die Some-Werte. Aus der gemischten Eingabe mit gültigen und ungültigen Strings werden nur die gültigen Zahlen extrahiert.
Diese Komposition aus filter_map und parse().ok() ist ein sehr typisches Idiom in Rust-Code für „nimm eine Liste von Strings, behalte nur die parsbaren als Zahlen". Drei Zeilen für etwas, das in vielen Sprachen mehrere Schritte mit explizitem Try/Catch-Boilerplate bräuchte.
Field-Update mit take
pub struct Buffer { inhalt: Option<Vec<u8>> }
impl Buffer {
pub fn entnehmen(&mut self) -> Vec<u8> {
self.inhalt.take().unwrap_or_default()
}
pub fn schreiben(&mut self, daten: Vec<u8>) {
self.inhalt = Some(daten);
}
}
fn main() {
let mut b = Buffer { inhalt: Some(vec![1, 2, 3]) };
let d = b.entnehmen();
assert_eq!(d, vec![1, 2, 3]);
assert!(b.inhalt.is_none());
}take() extrahiert den Wert aus dem Option und setzt es auf None — ohne self zu verbrauchen.
Optionale Funktion-Parameter
pub fn baue_url(host: &str, port: Option<u16>) -> String {
match port {
Some(p) => format!("https://{host}:{p}"),
None => format!("https://{host}"),
}
}
fn main() {
assert_eq!(baue_url("example.com", None), "https://example.com");
assert_eq!(baue_url("example.com", Some(8443)), "https://example.com:8443");
}Explizite Optionalität in der Signatur — kein „magisches null".
Frühe Validierung mit ? und ok_or
pub fn extrahiere_domain(email: &str) -> Result<&str, String> {
let at_position = email.find('@').ok_or_else(|| String::from("kein @"))?;
let domain = &email[at_position + 1..];
if domain.is_empty() {
return Err(String::from("Domain leer"));
}
Ok(domain)
}find gibt Option, ok_or macht es zu Result, ? propagiert beim Err. Sehr kompakte Validierungs-Pipeline.
Default + Override
pub fn timeout_ms(custom: Option<u64>) -> u64 {
custom.unwrap_or(5_000)
}
pub fn timeout_ms_v2(custom: Option<u64>) -> u64 {
custom.unwrap_or_else(|| {
// teure Default-Berechnung (z. B. aus Config)
aus_config()
})
}
fn aus_config() -> u64 { 5_000 }unwrap_or mit konstantem Default, unwrap_or_else mit lazy berechnetem Default.
Optional verschachtelt — flatten
pub fn finde_user_email(users: &[(u64, Option<String>)], id: u64) -> Option<&String> {
users.iter()
.find(|(uid, _)| *uid == id) // Option<&(u64, Option<String>)>
.map(|(_, mail)| mail.as_ref()) // Option<Option<&String>>
.flatten() // Option<&String>
}
fn main() {
let users = vec![
(1, Some("anna@example.com".to_string())),
(2, None),
];
assert!(finde_user_email(&users, 1).is_some());
assert!(finde_user_email(&users, 2).is_none());
assert!(finde_user_email(&users, 99).is_none());
}flatten() macht aus Option<Option<T>> ein Option<T> — sehr nützlich bei verschachtelten Optionen.
FAQ
Wann unwrap?
Nur in Tests, Beispielen, oder wenn du beweisen kannst, dass None unmöglich ist. Im normalen Code: ?, unwrap_or, if let, match. expect("warum None hier unmöglich ist") ist besser als unwrap, weil der Panic mit Kontext kommt.
unwrap_or oder unwrap_or_else?
unwrap_or(default) wenn default billig ist (eine Konstante, ein Zero-Wert). unwrap_or_else(|| ...) wenn default eine teure Berechnung oder Allocation ist — dann wird sie nur bei None ausgewertet.
?-Operator auf Option vs. Result.
? funktioniert auf beiden — abhängig vom Rückgabe-Typ der umliegenden Funktion. Bei fn foo() -> Option<T> propagiert ? ein None. Bei fn foo() -> Result<T, E> propagiert ? ein Err. Mischformen brauchen ok_or zur Konvertierung.
Niche-Optimization macht Option<&T> kostenlos.
Option<&T> ist 8 Bytes auf 64-bit, identisch zu &T. Der Null-Pointer dient als None. Damit ist Option<&T> so effizient wie ein nullable Pointer in C — aber typsicher prüfbar.
map vs. and_then.
.map(f) wenn f ein normaler Wert zurückgibt: Option<T> → Option<U>. .and_then(f) wenn f selbst ein Option<U> zurückgibt — sonst hättest du Option<Option<U>>. Auch bekannt als „flatMap" in anderen Sprachen.
filter macht aus Some(v) None, wenn Predikat false.
Some(42).filter(|&n| n > 100) ergibt None. Nützlich für „nimm den Wert nur, wenn ...". Kombiniert mit map und and_then zu Functional-Style-Pipelines.
take ist für &mut self-Methoden Gold.
Wenn du in einer Methode mit &mut self-Receiver einen Option<Vec<...>>-Wert herausnehmen willst (ohne self zu verbrauchen), ist take() die Lösung. Standard-Pattern für „Field auf None setzen und alten Wert verarbeiten".
Vermeide if x.is_some() { x.unwrap() }.
Anti-Pattern. Stattdessen: if let Some(v) = x { ... }. Genau ein Pattern-Match statt zwei separate Checks. Clippy warnt darauf.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – The Option Enum
- std::option
- std::option::Option Methoden
- Null References: The Billion Dollar Mistake