Rust bietet zwei Float-Typen — f32 (32 bit) und f64 (64 bit, Default). Beide folgen dem IEEE-754-Standard, was bedeutet: sie können NaN (Not a Number), +Infinity und -Infinity annehmen, und Gleichheit zwischen Floats ist eine subtile Sache. Dieser Artikel zeigt, wie Floats in Rust funktionieren, warum sie nur PartialOrd (nicht Ord) sind, wie man sicher vergleicht und welche Methoden in der Standard-Library bereitstehen.
f32 und f64 — was sie unterscheidet
| Aspekt | f32 | f64 |
|---|---|---|
| Bitbreite | 32 | 64 |
| Signifikante Stellen | ≈ 7 dezimal | ≈ 15-16 dezimal |
| Wertebereich | ±3,4 · 10³⁸ | ±1,8 · 10³⁰⁸ |
| Default-Typ | nein | ja |
| Performance | etwas schneller auf 32-bit-Hardware | identisch oder schneller auf modernen x86_64 |
| Speicher | 4 Bytes | 8 Bytes |
| Anwendung | Grafik, ML mit halber Präzision | sonst alles |
In der überwiegenden Mehrheit aller Anwendungsfälle ist f64 die richtige Wahl. f32 lohnt sich gezielt bei:
- Grafik/Rendering — GPUs arbeiten oft nativ in
f32. - Speicher-kritischen Vektoren — Audio-Samples, große Mess-Datensätze.
- WebAssembly — kleinere Daten reduzieren Transfer- und Speicherkosten.
Literale und Suffixe
let a = 3.14; // f64 (Default)
let b = 3.14f32; // f32
let c = 1_000_000.5; // f64, mit Unterstrich
let d = 2.5e10; // f64, Exponenten-Schreibweise = 25_000_000_000.0
let e = 1.0f32; // f32
let f: f32 = 1.0; // f32 durch Annotation
let g = 0.0; // f64 (Default)Zwei wichtige Eigenheiten:
- Ein Punkt erzeugt automatisch einen Float:
1.0istf64,1isti32.1.allein ist gültig undf64. - Float-Literale benötigen einen Punkt oder Exponenten:
let x: f64 = 5;ist ein Fehler. Stattdessen5.0oder5_f64.
NaN und Infinity
IEEE-754 reserviert spezielle Float-Werte, die in der Arithmetik auftreten können:
let nan = f64::NAN;
let pos_inf = f64::INFINITY;
let neg_inf = f64::NEG_INFINITY;
println!("{nan} {pos_inf} {neg_inf}");
// NaN inf -inf
// Entsteht u.a. so:
let a = 0.0_f64 / 0.0; // NaN
let b = 1.0_f64 / 0.0; // inf
let c = (-1.0_f64).sqrt(); // NaN (negativer Wert unter Wurzel)Prüfen, ob ein Wert NaN oder Infinity ist:
let x = 0.0_f64 / 0.0;
println!("{}", x.is_nan()); // true
println!("{}", x.is_finite()); // false
let y: f64 = 1.0 / 0.0;
println!("{}", y.is_infinite()); // trueWarum nur PartialOrd, nicht Ord?
Floats können nicht total geordnet werden — wegen NaN. Per IEEE-754 ist NaN != NaN, NaN < x ist false für jedes x, und NaN > x ebenfalls.
Das ist der Grund, warum f64 zwar PartialOrd implementiert (lieferte ein Option<Ordering>), aber nicht Ord (lieferte ein nicht-optionales Ordering).
Folge: viele generische Funktionen, die Ord verlangen, funktionieren nicht direkt mit Floats:
let mut werte: Vec<f64> = vec![3.0, 1.0, 2.0];
// werte.sort(); // Fehler — sort verlangt Ord
werte.sort_by(|a, b| a.partial_cmp(b).unwrap());
println!("{:?}", werte); // [1.0, 2.0, 3.0]partial_cmp gibt Option<Ordering> zurück; wenn beide Werte vergleichbar sind (also keiner NaN), kommt Some(Ordering) raus.
Mit Rust 1.50+ gibt es bequemere Methoden:
let mut werte: Vec<f64> = vec![3.0, f64::NAN, 1.0];
werte.sort_by(|a, b| a.total_cmp(b));
// total_cmp definiert eine totale Ordnung — auch mit NaNtotal_cmp sortiert auch mit NaN-Werten (per Konvention werden positive NaN am Ende einsortiert).
Float-Gleichheit — die zentrale Falle
== auf Floats ist legal, aber selten richtig:
fn main() {
let a = 0.1_f64 + 0.2_f64;
let b = 0.3_f64;
println!("{a} == {b}: {}", a == b);
// 0.30000000000000004 == 0.3: false
}Floats sind binäre Annäherungen. 0.1 lässt sich nicht exakt als binäre Fraktion darstellen, also tritt bei jeder Operation ein winziger Fehler auf.
Sichere Gleichheit mit Epsilon
fn nahe_gleich(a: f64, b: f64, epsilon: f64) -> bool {
(a - b).abs() < epsilon
}
fn main() {
let a = 0.1 + 0.2;
println!("{}", nahe_gleich(a, 0.3, 1e-10)); // true
}Für robustere Vergleiche existieren Crates wie approx mit relativen und absoluten Epsilon-Vergleichen. Für die meisten Anwendungsfälle reicht aber ein einfaches < 1e-10.
f64::EPSILON ist die kleinste positive Zahl, sodass 1.0 + EPSILON != 1.0 — also etwa 2.22 · 10⁻¹⁶. Sinnvoll als Untergrenze für relative Vergleiche, aber nicht als „magic number" für jeden Float-Vergleich.
Wichtige Methoden auf f64
let x = 2.0_f64;
x.sqrt(); // 1.4142135623730951
x.powi(10); // 1024.0 — Potenz mit Integer-Exponent
x.powf(2.5); // 5.656854249492381 — Potenz mit Float-Exponent
x.exp(); // e^x = 7.389056098930650
x.ln(); // natürlicher Logarithmus
x.log10(); // 0.301029995663981
x.log2(); // 1.0
x.sin(); x.cos(); x.tan(); // Trigonometrie (Bogenmaß)
x.asin(); x.acos(); x.atan();
(1.0_f64).atan2(1.0); // 0.785... = π/4
x.floor(); // Aufrunden zur niedrigeren ganzen Zahl
x.ceil(); // Aufrunden zur höheren
x.round(); // Klassisches Runden
x.trunc(); // In Richtung 0 abschneiden
x.fract(); // Nur Nachkommateil
(-3.5_f64).abs(); // 3.5
(3.7_f64).max(5.2); // 5.2
(3.7_f64).min(5.2); // 3.7std::f64::consts::PI; // 3.141592653589793
std::f64::consts::E; // 2.718281828459045
std::f64::consts::TAU; // 2π = 6.283...
std::f64::consts::SQRT_2; // √2 = 1.414...
std::f64::consts::LN_2; // ln(2) = 0.693...
f64::MAX; // 1.7976931348623157e308
f64::MIN; // -1.7976931348623157e308
f64::EPSILON; // 2.220446049250313e-16
f64::INFINITY;
f64::NAN;Float ↔ Integer
Konvertierungen zwischen Float und Integer brauchen as (oder die sichereren Konvertierungs-Traits):
let f = 3.7_f64;
let i = f as i32; // 3 — schneidet Nachkommateil ab (Richtung 0)
let neg = -3.7_f64;
let i_neg = neg as i32; // -3 — Richtung 0
let zu_gross = 1.0e20_f64;
let i_max = zu_gross as i32; // i32::MAX = 2_147_483_647 (saturiert!)
let nan = f64::NAN;
let i_nan = nan as i32; // 0 — NaN wird zu 0as von Float zu Integer hat seit Rust 1.45 deterministisches Saturieren — Werte außerhalb des Integer-Bereichs werden an die Limits angedockt, NaN wird zu 0. Vorher war es Undefined Behavior, was zu subtilen Bugs führte.
Integer-zu-Float ist meist verlustfrei für kleine Werte:
let i = 1000_i32;
let f = i as f64; // 1000.0, verlustfrei
let f2 = i as f32; // 1000.0, verlustfrei
let gross = i64::MAX;
let f3 = gross as f64; // verlustig — i64 hat 63 Bit Mantisse, f64 nur 52Praxis: Wo Floats wirklich gebraucht werden
Geo-Distanz mit Haversine-Formel
Zwei Geo-Koordinaten in Kilometern messen — Standard-Pattern in jeder Karten- oder Navigation-Anwendung.
const ERDRADIUS_KM: f64 = 6371.0;
fn haversine(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let d_lat = (lat2 - lat1).to_radians();
let d_lon = (lon2 - lon1).to_radians();
let lat1 = lat1.to_radians();
let lat2 = lat2.to_radians();
let a = (d_lat / 2.0).sin().powi(2)
+ lat1.cos() * lat2.cos() * (d_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
ERDRADIUS_KM * c
}
fn main() {
// Berlin -> München
let km = haversine(52.5200, 13.4050, 48.1351, 11.5820);
println!("{km:.1} km"); // ~504 km
}f64 hier zwingend — f32 würde bei Werten nahe der Erd-Skala Präzision verlieren.
3D-Vektor-Operationen
Klassisches Pattern in Spiele-Engines, Physik-Simulationen und Computer-Grafik:
#[derive(Clone, Copy)]
struct Vec3 { x: f32, y: f32, z: f32 }
impl Vec3 {
fn laenge(self) -> f32 {
(self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
}
fn normiert(self) -> Vec3 {
let l = self.laenge();
Vec3 { x: self.x / l, y: self.y / l, z: self.z / l }
}
fn dot(self, other: Vec3) -> f32 {
self.x * other.x + self.y * other.y + self.z * other.z
}
}f32 ist hier idiomatisch — Grafik-APIs (Vulkan, Metal, wgpu) arbeiten überwiegend in f32, und für 3D-Koordinaten ist die Präzision ausreichend.
Streaming-Statistik
Online-Mittelwert und Varianz mit Welfords Algorithmus — nützlich für Monitoring, Sensor-Daten, ML-Feature-Aggregation:
struct Stats {
count: u64,
mean: f64,
m2: f64,
}
impl Stats {
fn new() -> Stats {
Stats { count: 0, mean: 0.0, m2: 0.0 }
}
fn beobachte(&mut self, x: f64) {
self.count += 1;
let delta = x - self.mean;
self.mean += delta / self.count as f64;
let delta2 = x - self.mean;
self.m2 += delta * delta2;
}
fn varianz(&self) -> f64 {
if self.count < 2 { 0.0 } else { self.m2 / (self.count - 1) as f64 }
}
}Vorteil gegenüber dem naiven „Summe und Summe-der-Quadrate"-Ansatz: numerisch stabil auch bei Millionen von Werten. Float-Präzisionsverluste sammeln sich nicht akkumulativ.
Häufige Stolperfallen
== auf Floats ist (fast) immer falsch.
Außer in sehr spezifischen Fällen (Vergleich mit 0.0, Vergleich gegen einen anderen, gerade berechneten Wert ohne Zwischenoperationen) führt == zu Bugs. Lösung: Epsilon-Vergleiche mit (a - b).abs() < epsilon oder die approx-Crate für robustere Patterns.
NaN != NaN — auch nicht mit sich selbst.
IEEE-754 spezifiziert das. Resultat in Rust: f64::NAN == f64::NAN ist false. Tests gegen NaN sollten x.is_nan() nutzen, nicht x == f64::NAN. Auch in HashMap-Schlüsseln sind Floats problematisch (kein Eq), weshalb es die Crate ordered-float gibt.
Float-Sortierung verlangt partial_cmp oder total_cmp.
vec.sort() funktioniert nicht direkt. Lösung: vec.sort_by(|a, b| a.total_cmp(b)) für seit Rust 1.62 verfügbare totale Ordnung mit NaN-Behandlung, oder sort_by(|a, b| a.partial_cmp(b).unwrap()) mit Panic-Risiko bei NaN.
0.1 + 0.2 != 0.3 ist kein Rust-Bug.
Das gleiche Verhalten findest du in JavaScript, Python, C, Java. Es ist eine IEEE-754-Eigenschaft. Wenn dezimale Präzision (Geld!) wichtig ist, nimm ein Decimal-Crate wie rust_decimal — oder rechne in „Cent" mit Integer.
powi ist schneller als powf.
Für ganzzahlige Exponenten: x.powi(3) statt x.powf(3.0). Erster nutzt iterative Multiplikation, zweiter eine teurere Expo-Logarithmus-Formel. Faktor 2–5 Performance-Unterschied bei Hot-Path-Berechnungen.
Float-zu-Integer kann saturieren — das war nicht immer so.
Vor Rust 1.45 war (f64::INFINITY as i32) undefined behavior. Seit 1.45 ist das deterministisch: INFINITY → i32::MAX, -INFINITY → i32::MIN, NaN → 0. Wer auf älteren Compilern unterwegs ist, sollte das im Hinterkopf behalten — heutiger Code ist sicher.
f32::EPSILON ist nicht „der kleinste Float“.
EPSILON ist die Lücke zwischen 1.0 und dem nächsten darstellbaren Float — also etwa 1.19 · 10⁻⁷ für f32. Der kleinste positive Float ist f32::MIN_POSITIVE (≈ 1.18 · 10⁻³⁸), der allerkleinste subnormale Float ist f32::MIN_POSITIVE.next_down(). Wichtig zu wissen, wenn man absolute Vergleiche kalibriert.
Floats in HashMaps oder als Eq-Typ sind verboten.
Da f64 nicht Eq implementiert (nur PartialEq), kann man HashMap<f64, V> nicht direkt nutzen. Die Crate ordered-float liefert einen Ord-fähigen Wrapper. Für absolute Werte als Schlüssel: lieber in Integer-Repräsentation umwandeln (z. B. „Cent" statt „Euro").
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Floating-Point Types
- Rust Reference – Floating-point types
- std::f64 – Methoden-Doku
- std::f64::consts – Mathematische Konstanten
- IEEE 754 (Wikipedia)
- The crate
approx– robuste Float-Vergleiche