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.
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() extrahiert den Wert oder panickt. Nur in Tests, Prototypen, oder wenn du beweisbar sicher bist, dass None unmöglich ist.
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}");
}Wie unwrap, aber mit klarer Fehler-Meldung. Idiomatisch für „Hier ist None unmöglich, weil...".
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) — der Default-Wert wird sofort ausgewertet, auch wenn Some vorliegt.
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}");
}Mit Closure — wird nur ausgewertet bei None. Wichtig, wenn der Default teuer zu konstruieren ist.
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 transformiert den inneren Wert, falls vorhanden. Klassisches Funktional-Pattern.
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 chaint Option-Operationen. Die nächste Closure läuft nur, wenn der vorherige Some ergab.
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(err) baut aus Option<T> ein Result<T, E> — sehr nützlich am Übergang von Optional zu Fallible.
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);
}? macht aus Option<T> ein T (wenn Some) oder propagiert None als Funktions-Rückgabe. Sehr kompakt für Chains, in denen jeder Schritt fehlschlagen kann.
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 sehr nützlich, wenn du den Option-Wert nicht verbrauchen willst.
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() nimmt den Wert aus dem Option heraus und ersetzt durch None. Sehr nützlich, wenn du in einer &mut self-Methode den Wert haben willst, ohne self zu verbrauchen.
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");
}
}HashMap::get gibt Option<&V>. Mit .map(...) weiter verarbeitet.
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);
}find plus map — sehr typisches Such-Pattern.
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]);
}parse() gibt Result, .ok() macht es zu Option. filter_map collect't nur die Some-Werte — eine sehr elegante Filter-und-Transform-Operation.
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, &'static str> {
let at_position = email.find('@').ok_or("kein @")?;
let domain = &email[at_position + 1..];
if domain.is_empty() {
return Err("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