Integer-Überlauf in Rust
Jeder Ganzzahltyp in Rust hat eine feste Größe. Ein u8 hat Platz für die Zahlen von 0 bis 255 und für nichts sonst. Was passiert also, wenn du 255 hast und 1 dazuzählst? Das Ergebnis passt nicht hinein. Das nennt man Integer Overflow (Ganzzahlüberlauf).
Denk an den Kilometerzähler eines alten Autos mit nur drei Ziffern. Nach 999 Kilometern springt er zurück auf 000. Für ein Spielzeug ist das in Ordnung, für einen Kontostand ist es fatal. Rust hilft dir hier auf zwei Arten. Es meldet das Problem laut, während du entwickelst. Und es gibt dir einfache Methoden, mit denen du genau festlegst, was stattdessen passieren soll. Keine Sorge, es sind nur vier, und alle folgen demselben Muster. Diese Seite baut auf Datentypen und Operatoren auf.
Wie groß ist welcher Ganzzahltyp?
Abschnitt betitelt „Wie groß ist welcher Ganzzahltyp?“Jeder Typ hat einen kleinsten Wert (MIN) und einen größten Wert (MAX). Du kannst sie mit Konstanten wie u8::MAX oder i32::MIN ausgeben:
fn main() { println!("u8: {} bis {}", u8::MIN, u8::MAX); println!("i8: {} bis {}", i8::MIN, i8::MAX); println!("u16: {} bis {}", u16::MIN, u16::MAX); println!("i32: {} bis {}", i32::MIN, i32::MAX); println!("u32: {} bis {}", u32::MIN, u32::MAX); println!("i64: {} bis {}", i64::MIN, i64::MAX); println!("u64: {} bis {}", u64::MIN, u64::MAX); println!("u128: {} bis {}", u128::MIN, u128::MAX);}Ausgabe:
u8: 0 bis 255i8: -128 bis 127u16: 0 bis 65535i32: -2147483648 bis 2147483647u32: 0 bis 4294967295i64: -9223372036854775808 bis 9223372036854775807u64: 0 bis 18446744073709551615u128: 0 bis 340282366920938463463374607431768211455Hier ist die ganze Familie in einer Tabelle:
| Typ | Bits | Kleinster Wert | Größter Wert |
|---|---|---|---|
i8 / u8 |
8 | -128 / 0 | 127 / 255 |
i16 / u16 |
16 | -32.768 / 0 | 32.767 / 65.535 |
i32 / u32 |
32 | etwa -2,1 Milliarden / 0 | etwa 2,1 Milliarden / 4,3 Milliarden |
i64 / u64 |
64 | etwa -9,2 × 10^18 / 0 | etwa 9,2 × 10^18 / 1,8 × 10^19 |
i128 / u128 |
128 | etwa -1,7 × 10^38 / 0 | etwa 1,7 × 10^38 / 3,4 × 10^38 |
isize / usize |
64 auf den meisten Computern | wie i64 / 0 |
wie i64 / u64 |
Ein vorzeichenbehafteter Typ (i) verwendet die Hälfte seines Bereichs für negative Zahlen. Ein vorzeichenloser Typ (u) beginnt bei 0, deshalb ist sein größter Wert etwa doppelt so groß. Alles außerhalb dieser Bereiche ist ein Überlauf. Auch wer unter MIN fällt (etwa 0 - 1 bei einem u8), erzeugt einen. Das nennt man manchmal Unterlauf (Underflow), aber Rust behandelt beides gleich.
Was Rust bei einem Überlauf macht
Abschnitt betitelt „Was Rust bei einem Überlauf macht“In Debug-Builds gibt es eine Panic
Abschnitt betitelt „In Debug-Builds gibt es eine Panic“Startest du dein Programm mit cargo run (ein Debug-Build), prüft Rust jedes +, - und *. Passt das Ergebnis nicht, hält das Programm mit einer klaren Meldung an. Hier addiert ein Spiel Bonuspunkte zu einem Punktestand vom Typ u8:
fn add_points(score: u8, bonus: u8) -> u8 { score + bonus}
fn main() { let total = add_points(250, 10); println!("Summe: {total}");}Wenn du es ausführst, bekommst du:
thread 'main' (12345) panicked at src/main.rs:2:5:attempt to add with overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceRust meldet hier auf Englisch „attempt to add with overflow“, also „Versuch einer Addition mit Überlauf“, und nennt dir Datei, Zeile und Spalte. Die Werte laufen absichtlich über Funktionsparameter. Würdest du die Zahlen direkt neben das + schreiben, würde der Compiler das Problem schon vor dem Start entdecken (das siehst du gleich). Ein Absturz wirkt vielleicht hart, ist aber ein Gefallen: Du findest den Fehler auf deinem eigenen Rechner, mit der genauen Zeilennummer, statt in der Produktion einen falschen Punktestand zu bekommen. Mehr über Panics erfährst du auf der Seite Panic.
In Release-Builds läuft die Zahl über
Abschnitt betitelt „In Release-Builds läuft die Zahl über“Baust du mit cargo run --release, sind diese Prüfungen abgeschaltet, damit der Code ein bisschen schneller läuft. Die Zahl läuft über (Wrap-around) wie der Kilometerzähler: 250 + 10 wird bei einem u8 zu 4 (denn 260 - 256 = 4).
cargo run --releaseSumme: 4Keine Panic, keine Warnung, nur eine stillschweigend falsche Zahl. Deshalb solltest du dich nie darauf verlassen, dass ein Überlauf „zufällig“ passiert. Willst du, dass die Zahl überläuft, sag es ausdrücklich mit wrapping_add (siehe unten). Die „Try it“-Buttons auf dieser Seite führen Debug-Builds aus. Mit wrapping_add kannst du dieses Verhalten also auch dort sehen:
fn main() { let score: u8 = 250; println!("{}", score.wrapping_add(10));}Ausgabe:
4Sollen auch Release-Builds eine Panic auslösen, kannst du die Prüfungen in der Cargo.toml wieder einschalten:
[profile.release]overflow-checks = trueMit Zahlen direkt im Code kompiliert es gar nicht erst
Abschnitt betitelt „Mit Zahlen direkt im Code kompiliert es gar nicht erst“Sieht der Compiler die ganze Rechnung, weigert er sich, sie zu bauen:
fn main() { let level: u8 = 255 + 1; println!("{level}");}error: this arithmetic operation will overflow --> src/main.rs:2:21 |2 | let level: u8 = 255 + 1; | ^^^^^^^ attempt to compute `u8::MAX + 1_u8`, which would overflow | = note: `#[deny(arithmetic_overflow)]` on by defaultDer Compiler sagt: „Diese Rechenoperation wird überlaufen.“ Dasselbe gilt für ein Literal, das einfach zu groß ist, etwa let level: u8 = 256;. Das ergibt den Fehler literal out of range for u8 (Literal außerhalb des Bereichs von u8).
Das Verhalten selbst wählen
Abschnitt betitelt „Das Verhalten selbst wählen“Statt zu hoffen, dass nie ein Überlauf passiert, kannst du festlegen, was dann passieren soll. Jeder Ganzzahltyp hat dafür vier Familien von Methoden. Sie funktionieren bei u8, i32, u64 und allen anderen gleich.
checked_: „Sag mir, wenn es nicht gepasst hat“
Abschnitt betitelt „checked_: „Sag mir, wenn es nicht gepasst hat““checked_add, checked_sub und checked_mul geben ein Option zurück: Some(ergebnis), wenn es passt, und None, wenn nicht. Das ist die sicherste Wahl, wann immer eine falsche Zahl ein echtes Problem wäre, zum Beispiel bei Geld. Ein Kontostand kann nicht unter 0 fallen. Ein u64 mit checked_sub lehnt die Abhebung also für dich ab:
fn withdraw(balance: u64, amount: u64) -> Option<u64> { balance.checked_sub(amount)}
fn order_total(price: u32, quantity: u32, shipping: u32) -> Option<u32> { price.checked_mul(quantity)?.checked_add(shipping)}
fn main() { let balance: u64 = 100;
for amount in [30, 250] { match withdraw(balance, amount) { Some(left) => println!("{amount} abgehoben, Kontostand jetzt {left}"), None => println!("{amount} kann nicht abgehoben werden: nicht genug Geld"), } }
println!("{:?}", order_total(20, 3, 5)); println!("{:?}", order_total(2_000_000_000, 3, 5));}Ausgabe:
30 abgehoben, Kontostand jetzt 70250 kann nicht abgehoben werden: nicht genug GeldSome(65)NoneWeil das Ergebnis ein Option ist, funktioniert auch der ?-Operator. Läuft in order_total die Multiplikation über, gibt die Funktion sofort None zurück.
saturating_: „Bleib am Rand stehen“
Abschnitt betitelt „saturating_: „Bleib am Rand stehen““saturating_add und saturating_sub gehen nie über MIN oder MAX hinaus. Sie bleiben einfach dort stehen, wie ein volles Glas. Das ist perfekt für Dinge mit einer natürlichen Unter- oder Obergrenze: einen Lebensbalken, einen Lautstärkeregler, Artikel auf Lager.
fn main() { let health: u8 = 20; let after_hit = health.saturating_sub(35); println!("Lebenspunkte nach einem harten Treffer: {after_hit}");
let volume: u8 = 90; let louder = volume.saturating_add(25).min(100); println!("Lautstärke nach dem Aufdrehen: {louder}");
let stock: u32 = 3; let left = stock.saturating_sub(5); println!("Artikel noch auf Lager: {left}");}Ausgabe:
Lebenspunkte nach einem harten Treffer: 0Lautstärke nach dem Aufdrehen: 100Artikel noch auf Lager: 0Die Zeile mit der Lautstärke braucht einen Schritt mehr. saturating_add bleibt nur an der Grenze des Typs selbst stehen, bei einem u8 also bei 255. Dein Regler endet aber bei 100. Deshalb begrenzt .min(100) den Wert auf deine eigene Grenze.
wrapping_: „Lauf absichtlich über“
Abschnitt betitelt „wrapping_: „Lauf absichtlich über““Manchmal ist das Überlaufen genau das, was du willst. Ein Ticketzähler, der 254, 255, 0, 1 zählt. Oder eine einfache Hash-Funktion, bei der die Zahlen ohnehin durcheinandergewirbelt werden sollen. wrapping_add und wrapping_mul machen das in jedem Build, ob Debug oder Release:
fn simple_hash(text: &str) -> u32 { let mut hash: u32 = 5381; for byte in text.bytes() { hash = hash.wrapping_mul(33).wrapping_add(byte as u32); } hash}
fn main() { let mut ticket: u8 = 254; for _ in 0..4 { println!("Ticket {ticket}"); ticket = ticket.wrapping_add(1); }
println!("Hash von \"hello\": {}", simple_hash("hello")); println!("Hash von \"a much longer sentence\": {}", simple_hash("a much longer sentence"));}Ausgabe:
Ticket 254Ticket 255Ticket 0Ticket 1Hash von "hello": 261238937Hash von "a much longer sentence": 3373490575Die Texte, die gehasht werden, sind absichtlich auf Englisch geblieben: Der Hash wird aus den einzelnen Bytes berechnet, und andere Wörter (oder Umlaute, die in UTF-8 zwei Bytes belegen) ergäben andere Zahlen. Der lange Satz würde einen u32 mit normalem * und + gleich mehrfach überlaufen lassen. Mit den wrapping_-Methoden ist das kein Problem: Genau dafür ist der Hash gedacht.
overflowing_: „Gib mir beides“
Abschnitt betitelt „overflowing_: „Gib mir beides““overflowing_add gibt ein Tupel zurück: das übergelaufene Ergebnis und einen bool, der sagt, ob ein Überlauf passiert ist. Das ist praktisch, wenn du den übergelaufenen Wert willst, aber auch wissen möchtest, dass es passiert ist:
fn main() { let (value, overflowed) = 250u8.overflowing_add(10); println!("{value}, übergelaufen: {overflowed}");
let (value, overflowed) = 50u8.overflowing_add(10); println!("{value}, übergelaufen: {overflowed}");}Ausgabe:
4, übergelaufen: true60, übergelaufen: falseDiese Variante brauchst du seltener als die anderen. Wenn du dir nicht sicher bist, welche du nehmen sollst, ist checked_ eine sehr gute Standardwahl.
Überlauf beim Umwandeln mit as
Abschnitt betitelt „Überlauf beim Umwandeln mit as“Eine Typumwandlung mit as löst nie eine Panic aus, nicht einmal in Debug-Builds. Passt der Wert nicht, wird er stillschweigend zurechtgestutzt. Das überrascht viele, also sei vorsichtig damit:
fn main() { let big: i32 = 300; let negative: i32 = -1;
println!("300 as u8 = {}", big as u8); println!("-1 as u32 = {}", negative as u32);
match u8::try_from(big) { Ok(small) => println!("passt: {small}"), Err(e) => println!("300 passt nicht in einen u8: {e}"), }
match u8::try_from(200) { Ok(small) => println!("passt: {small}"), Err(e) => println!("200 passt nicht in einen u8: {e}"), }}Ausgabe:
300 as u8 = 44-1 as u32 = 4294967295300 passt nicht in einen u8: out of range integral type conversion attemptedpasst: 200Die Fehlermeldung „out of range integral type conversion attempted“ kommt von Rust selbst und bedeutet: Es wurde versucht, eine Ganzzahl in einen Typ umzuwandeln, dessen Bereich sie nicht abdeckt. 300 as u8 behält nur die untersten 8 Bits, und das ergibt 44. -1 as u32 wird zum größten u32. Keine der beiden Umwandlungen warnt dich. u8::try_from prüft dagegen zuerst und gibt dir ein Result, sodass du entscheiden kannst, was passieren soll. Eine gute Gewohnheit: Nimm as nur, wenn du weißt, dass der Wert passt, und try_from, wenn er von außerhalb deines Programms kommt. (Kommazahlen verhalten sich anders: 300.7 as u8 bleibt bei 255 stehen, statt überzulaufen.) Die Seiten Typumwandlung und Konvertierungen gehen tiefer darauf ein.
Weitere Überraschungen mit Zahlen
Abschnitt betitelt „Weitere Überraschungen mit Zahlen“Noch ein paar Dinge, über die viele stolpern, alle an einer Stelle:
fn main() { println!("7 / 2 = {}", 7 / 2); println!("-7 / 2 = {}", -7 / 2);
println!("i32::MIN.checked_abs() = {:?}", i32::MIN.checked_abs()); println!("i32::MIN.unsigned_abs() = {}", i32::MIN.unsigned_abs());
println!("10u32.checked_pow(9) = {:?}", 10u32.checked_pow(9)); println!("10u32.checked_pow(10) = {:?}", 10u32.checked_pow(10));}Ausgabe:
7 / 2 = 3-7 / 2 = -3i32::MIN.checked_abs() = Nonei32::MIN.unsigned_abs() = 214748364810u32.checked_pow(9) = Some(1000000000)10u32.checked_pow(10) = None- Ganzzahldivision lässt den Rest weg. Sie rundet nicht, sondern schneidet in Richtung null ab. Deshalb ist
-7 / 2gleich-3. Für den Rest nimmst du%, für Nachkommastellen Kommazahlen. abs()kann überlaufen. Eini32reicht hinunter bis -2147483648, aber nur hinauf bis 2147483647. Die positive Version voni32::MINpasst also nicht hinein.i32::MIN.abs()löst in Debug-Builds eine Panic mitattempt to negate with overflowaus (Versuch einer Negation mit Überlauf). Nimmchecked_abs()oderunsigned_abs(). Letzteres gibt einenu32zurück, in den der Wert passt.powwächst schnell. 10 hoch 10 ist schon zu groß für einenu32. Es gibt auchchecked_pow,saturating_powundwrapping_pow.
Soll ein Wert immer überlaufen oder immer am Rand stehen bleiben, kannst du ihn in std::num::Wrapping oder std::num::Saturating verpacken. Dann machen ganz normale + und - das Richtige, und du kannst es nicht vergessen:
use std::num::{Saturating, Wrapping};
fn main() { let mut ticket = Wrapping(254u8); ticket += 1; ticket += 1; ticket += 1; println!("Ticket: {ticket}");
let mut battery = Saturating(10u8); battery -= 25; println!("Akku: {battery}");}Ausgabe:
Ticket: 1Akku: 0Welche Methode soll ich nehmen?
Abschnitt betitelt „Welche Methode soll ich nehmen?“| Methodenfamilie | Gibt zurück | Nimm sie, wenn |
|---|---|---|
+ - * |
Die Zahl (Panic im Debug-Build, Überlauf im Release-Build) | du sicher bist, dass nichts überlaufen kann |
checked_add und Verwandte |
Option<T> |
eine falsche Zahl ein Fehler wäre: Geld, Größen, IDs |
saturating_add und Verwandte |
Die Zahl, festgehalten bei MIN oder MAX |
es eine natürliche Unter- oder Obergrenze gibt: Lebenspunkte, Lautstärke, Lagerbestand |
wrapping_add und Verwandte |
Die Zahl, übergelaufen | das Überlaufen gewollt ist: Hashes, Zähler, die im Kreis laufen |
overflowing_add und Verwandte |
(T, bool) |
du den übergelaufenen Wert willst und wissen möchtest, ob es passiert ist |
T::try_from(x) |
Result<T, _> |
du einen Wert umwandelst, der vielleicht nicht in einen kleineren Typ passt |
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“1 von der Länge einer leeren Liste abziehen
Abschnitt betitelt „1 von der Länge einer leeren Liste abziehen“len() gibt einen usize zurück, und der kann nicht negativ sein. Bei einer leeren Liste wäre len() - 1 gleich -1, also läuft es über:
fn last_index(tasks: &[String]) -> usize { tasks.len() - 1}
fn main() { let tasks: Vec<String> = Vec::new(); println!("die letzte Aufgabe steht an Index {}", last_index(&tasks));}Wenn du es ausführst, bekommst du:
thread 'main' (12345) panicked at src/main.rs:2:5:attempt to subtract with overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace„attempt to subtract with overflow“ heißt: Versuch einer Subtraktion mit Überlauf. Darauf fällt fast jeder mindestens einmal herein. Die Lösung: Lass den Typ ausdrücken, dass die Liste leer sein könnte. Gib tasks.len().checked_sub(1) zurück, das ist ein Option<usize>. Willst du nur das letzte Element, ist tasks.last() sogar noch einfacher.
Kleine Zahlen in einem kleinen Typ aufsummieren
Abschnitt betitelt „Kleine Zahlen in einem kleinen Typ aufsummieren“Drei Testergebnisse von 90, 85 und 100 passen jeweils in einen u8. Ihre Summe, 275, passt nicht. Deshalb löst let total: u8 = scores.iter().sum(); eine Panic mit attempt to add with overflow aus. Die Panic zeigt auf eine Stelle in der Standardbibliothek, was verwirren kann. Die Ursache ist aber dein u8. Wandle jeden Wert vor dem Addieren in einen größeren Typ um:
fn main() { let scores: Vec<u8> = vec![90, 85, 100]; let total: u32 = scores.iter().map(|&s| s as u32).sum(); println!("Summe: {total}, Durchschnitt: {}", total / 3);}Ausgabe:
Summe: 275, Durchschnitt: 91Von u8 nach u32 mit as umzuwandeln ist immer sicher, denn jeder u8 passt in einen u32.
Übungen
Abschnitt betitelt „Übungen“1. Ein Handy-Akku
Abschnitt betitelt „1. Ein Handy-Akku“Der Akkustand eines Handys ist ein u8 von 0 bis 100. Schreib charge(level: u8, amount: u8) -> u8 und drain(level: u8, amount: u8) -> u8, die nie über 100 steigen und nie unter 0 fallen. Starte bei 80, lade um 30 auf, entlade dann um 130 und gib nach jedem Schritt den Stand aus. Tipp: saturating_add plus .min(100) zum Laden, saturating_sub zum Entladen.
Lösung anzeigen
fn charge(level: u8, amount: u8) -> u8 { level.saturating_add(amount).min(100)}
fn drain(level: u8, amount: u8) -> u8 { level.saturating_sub(amount)}
fn main() { let mut level: u8 = 80; println!("Start: {level}%");
level = charge(level, 30); println!("nach dem Laden: {level}%");
level = drain(level, 130); println!("nach einem langen Tag: {level}%");}Ausgabe:
Start: 80%nach dem Laden: 100%nach einem langen Tag: 0%saturating_sub bleibt von selbst bei 0 stehen. Beim Laden liegt die Grenze des Typs bei 255, deshalb setzt .min(100) deine eigene Grenze obendrauf.
2. Eine Fakultät, die ihre Grenzen kennt
Abschnitt betitelt „2. Eine Fakultät, die ihre Grenzen kennt“Schreib fn factorial(n: u64) -> Option<u64>, die 1 × 2 × … × n multipliziert und None zurückgibt, wenn das Ergebnis zu groß für einen u64 wird. Gib das Ergebnis für 5, 20 und 21 aus. Tipp: Fang mit let mut result: u64 = 1; an und nimm in einer for-Schleife result.checked_mul(i)?.
Lösung anzeigen
fn factorial(n: u64) -> Option<u64> { let mut result: u64 = 1; for i in 1..=n { result = result.checked_mul(i)?; } Some(result)}
fn main() { for n in [5, 20, 21] { match factorial(n) { Some(value) => println!("{n}! = {value}"), None => println!("{n}! ist zu groß für einen u64"), } }}Ausgabe:
5! = 12020! = 243290200817664000021! ist zu groß für einen u64Das ? gibt None aus der ganzen Funktion zurück, sobald eine einzige Multiplikation nicht passt. 20! ist die größte Fakultät, die ein u64 aufnehmen kann.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Jeder Ganzzahltyp hat einen festen Bereich, von
T::MINbisT::MAX. Wer ihn verlässt, erzeugt einen Überlauf. - Debug-Builds lösen eine Panic mit
attempt to add with overflowaus. Release-Builds laufen stillschweigend über. Einen Überlauf, den der Compiler sehen kann, meldet er als Compilerfehler. - Wähl das Verhalten selbst:
checked_liefert einOption,saturating_bleibt am Rand stehen,wrapping_läuft über,overflowing_liefert den Wert und einenbool. asprüft nie und kann eine Zahl stillschweigend verändern. NimmT::try_from, wenn ein Wert vielleicht nicht passt.- Vorsicht bei
len() - 1auf leeren Listen, bei Summen in kleinen Typen, beiabs()vonMINund beipow.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was passiert mit 250u8 + 10 in einem Debug-Build, wenn die Werte aus Variablen kommen?
2. Was gibt 100u64.checked_sub(250) zurück?
3. Ein Lebensbalken soll nie unter 0 fallen. Welche Methode passt am besten?
4. Was ergibt 300_i32 as u8?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Ganzzahlüberlauf – Das Rust-Buch: die offizielle Erklärung zu Panics im Debug-Build, zum Überlaufen im Release-Build und zu den vier Methodenfamilien.
u8– Doku der Standardbibliothek (auf Englisch): allechecked_-,saturating_-,wrapping_- undoverflowing_-Methoden, jeweils mit einem kleinen Beispiel.i32– Doku der Standardbibliothek (auf Englisch): dieselben Methoden für vorzeichenbehaftete Zahlen, dazuchecked_abs,unsigned_absundchecked_pow.- Overflow – The Rust Reference (auf Englisch): die genauen Regeln, welche Operationen überlaufen können und was dann passiert.
- Casting – Rust by Example (auf Englisch): ausführbare Beispiele für
as-Umwandlungen, die überlaufen, abschneiden und am Rand stehen bleiben.