Overflow degli interi in Rust
Ogni tipo intero in Rust ha una dimensione fissa. Un u8 ha spazio per i numeri da 0 a 255, e nient’altro. Quindi cosa succede se hai 255 e aggiungi 1? Il risultato non ci sta. Si chiama overflow degli interi (traboccamento).
Pensa al contachilometri di una vecchia auto con solo tre cifre. Dopo 999 chilometri torna a 000. Per un giocattolo va bene, ma per il saldo di un conto in banca è un disastro. Rust ti aiuta in due modi. Segnala il problema a gran voce mentre sviluppi. E ti dà metodi semplici per dire esattamente cosa deve succedere invece. Niente paura: sono solo quattro, e seguono tutti lo stesso schema. Questa pagina parte da Tipi di dati e Operatori.
Quanto è grande ogni tipo intero?
Sezione intitolata “Quanto è grande ogni tipo intero?”Ogni tipo ha un valore minimo (MIN) e un valore massimo (MAX). Puoi stamparli con costanti come u8::MAX o i32::MIN:
fn main() { println!("u8: da {} a {}", u8::MIN, u8::MAX); println!("i8: da {} a {}", i8::MIN, i8::MAX); println!("u16: da {} a {}", u16::MIN, u16::MAX); println!("i32: da {} a {}", i32::MIN, i32::MAX); println!("u32: da {} a {}", u32::MIN, u32::MAX); println!("i64: da {} a {}", i64::MIN, i64::MAX); println!("u64: da {} a {}", u64::MIN, u64::MAX); println!("u128: da {} a {}", u128::MIN, u128::MAX);}Output:
u8: da 0 a 255i8: da -128 a 127u16: da 0 a 65535i32: da -2147483648 a 2147483647u32: da 0 a 4294967295i64: da -9223372036854775808 a 9223372036854775807u64: da 0 a 18446744073709551615u128: da 0 a 340282366920938463463374607431768211455Ecco tutta la famiglia in una tabella:
| Tipo | Bit | Minimo | Massimo |
|---|---|---|---|
i8 / u8 |
8 | -128 / 0 | 127 / 255 |
i16 / u16 |
16 | -32.768 / 0 | 32.767 / 65.535 |
i32 / u32 |
32 | circa -2,1 miliardi / 0 | circa 2,1 miliardi / 4,3 miliardi |
i64 / u64 |
64 | circa -9,2 × 10^18 / 0 | circa 9,2 × 10^18 / 1,8 × 10^19 |
i128 / u128 |
128 | circa -1,7 × 10^38 / 0 | circa 1,7 × 10^38 / 3,4 × 10^38 |
isize / usize |
64 sulla maggior parte dei computer | come i64 / 0 |
come i64 / u64 |
Un tipo con segno (i) usa metà del suo intervallo per i numeri negativi. Un tipo senza segno (u) parte da 0, quindi il suo valore massimo è circa il doppio. Tutto quello che sta fuori da questi intervalli è un overflow, e conta anche scendere sotto MIN (come 0 - 1 su un u8). Quest’ultimo a volte si chiama underflow, ma Rust lo tratta allo stesso modo.
Cosa fa Rust quando un numero va in overflow
Sezione intitolata “Cosa fa Rust quando un numero va in overflow”Nelle build di debug va in panic
Sezione intitolata “Nelle build di debug va in panic”Quando esegui il programma con cargo run (una build di debug), Rust controlla ogni +, - e *. Se il risultato non ci sta, il programma si ferma con un messaggio chiaro. Qui un gioco aggiunge punti bonus a un punteggio u8:
fn add_points(score: u8, bonus: u8) -> u8 { score + bonus}
fn main() { let total = add_points(250, 10); println!("totale: {total}");}Quando lo esegui, ottieni:
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 backtraceIl messaggio (in inglese, come tutti quelli di Rust) dice che il thread main è andato in panic alla riga 2 per un «tentativo di somma con overflow». I valori passano apposta dai parametri di una funzione. Se scrivessi i numeri proprio accanto al +, il compilatore vedrebbe il problema prima ancora che il programma parta (lo vedrai tra poco). Un crash può sembrare brutale, ma è una gentilezza: trovi il bug sul tuo computer, con il numero di riga esatto, invece di ritrovarti un punteggio sbagliato in produzione. Trovi di più sui panic nella pagina Panic.
Nelle build di release fa il giro
Sezione intitolata “Nelle build di release fa il giro”Quando compili con cargo run --release, quei controlli vengono disattivati per rendere il codice un po’ più veloce. Il numero fa il giro (wrap around) come il contachilometri: 250 + 10 su un u8 diventa 4 (perché 260 - 256 = 4).
cargo run --releasetotale: 4Niente panic, niente avvisi: solo un numero sbagliato in silenzio. Ecco perché non dovresti contare su un overflow che succede «per caso». Se vuoi che il numero faccia il giro, dillo con wrapping_add (lo vedi più sotto). I pulsanti «Provalo tu» di questo sito eseguono build di debug, quindi wrapping_add è anche il modo per vedere lì questo comportamento:
fn main() { let score: u8 = 250; println!("{}", score.wrapping_add(10));}Output:
4Se vuoi che anche le build di release vadano in panic, puoi riattivare i controlli in Cargo.toml:
[profile.release]overflow-checks = trueCon numeri scritti direttamente non compila nemmeno
Sezione intitolata “Con numeri scritti direttamente non compila nemmeno”Quando il compilatore vede tutto il calcolo, si rifiuta di compilarlo:
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 defaultIl compilatore avvisa che questa operazione aritmetica andrà in overflow: u8::MAX + 1 non ci sta. Lo stesso vale per un letterale semplicemente troppo grande, come let level: u8 = 256;, che dà literal out of range for u8 (letterale fuori dall’intervallo di u8).
Scegliere tu il comportamento
Sezione intitolata “Scegliere tu il comportamento”Invece di sperare che l’overflow non succeda mai, puoi scegliere cosa deve succedere. Ogni tipo intero ha quattro famiglie di metodi per questo. Funzionano tutte allo stesso modo su u8, i32, u64 e gli altri.
checked_: «dimmi se non ci sta»
Sezione intitolata “checked_: «dimmi se non ci sta»”checked_add, checked_sub e checked_mul restituiscono un Option: Some(risultato) se ci sta, None se non ci sta. È la scelta più sicura ogni volta che un numero sbagliato sarebbe un problema serio, come con i soldi. Il saldo di un conto non può scendere sotto 0, quindi un u64 con checked_sub rifiuta il prelievo al posto tuo:
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!("Prelevati {amount}, ora il saldo è {left}"), None => println!("Impossibile prelevare {amount}: soldi insufficienti"), } }
println!("{:?}", order_total(20, 3, 5)); println!("{:?}", order_total(2_000_000_000, 3, 5));}Output:
Prelevati 30, ora il saldo è 70Impossibile prelevare 250: soldi insufficientiSome(65)NoneSiccome il risultato è un Option, funziona anche l’operatore ?. In order_total, se la moltiplicazione va in overflow, la funzione restituisce subito None.
saturating_: «fermati al limite»
Sezione intitolata “saturating_: «fermati al limite»”saturating_add e saturating_sub non vanno mai oltre MIN o MAX. Si fermano lì, come un bicchiere pieno. È perfetto per le cose che hanno un minimo o un massimo naturale: una barra della salute, una manopola del volume, gli articoli in magazzino.
fn main() { let health: u8 = 20; let after_hit = health.saturating_sub(35); println!("salute dopo un colpo forte: {after_hit}");
let volume: u8 = 90; let louder = volume.saturating_add(25).min(100); println!("volume dopo averlo alzato: {louder}");
let stock: u32 = 3; let left = stock.saturating_sub(5); println!("articoli rimasti in magazzino: {left}");}Output:
salute dopo un colpo forte: 0volume dopo averlo alzato: 100articoli rimasti in magazzino: 0La riga del volume ha bisogno di un passo in più. saturating_add si ferma solo al limite del tipo stesso, 255 per un u8. La tua manopola si ferma a 100, quindi .min(100) la blocca al tuo limite.
wrapping_: «fai il giro apposta»
Sezione intitolata “wrapping_: «fai il giro apposta»”A volte fare il giro è proprio quello che vuoi. Un contatore di biglietti che va 254, 255, 0, 1. Oppure una semplice funzione di hash, dove i numeri devono rimescolarsi. wrapping_add e wrapping_mul fanno così in ogni build, di debug o di 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!("biglietto {ticket}"); ticket = ticket.wrapping_add(1); }
println!("hash di \"hello\": {}", simple_hash("hello")); println!("hash di \"a much longer sentence\": {}", simple_hash("a much longer sentence"));}Output:
biglietto 254biglietto 255biglietto 0biglietto 1hash di "hello": 261238937hash di "a much longer sentence": 3373490575I testi passati all’hash sono rimasti in inglese apposta: l’hash dipende da ogni singolo byte, quindi con parole diverse otterresti numeri diversi. La frase lunga, con i normali * e +, farebbe andare in overflow un u32 molte volte. Con i metodi wrapping_ va benissimo: è proprio quello che l’hash deve fare.
overflowing_: «dammi tutte e due le cose»
Sezione intitolata “overflowing_: «dammi tutte e due le cose»”overflowing_add restituisce una tupla: il risultato dopo il giro e un bool che dice se c’è stato overflow. È comodo quando vuoi il valore dopo il giro ma vuoi anche sapere che è successo:
fn main() { let (value, overflowed) = 250u8.overflowing_add(10); println!("{value}, overflow: {overflowed}");
let (value, overflowed) = 50u8.overflowing_add(10); println!("{value}, overflow: {overflowed}");}Output:
4, overflow: true60, overflow: falseUserai questo meno spesso degli altri. Se non sai quale scegliere, checked_ è un’ottima scelta predefinita.
Overflow nelle conversioni con as
Sezione intitolata “Overflow nelle conversioni con as”Convertire tra tipi con as non va mai in panic, nemmeno nelle build di debug. Se il valore non ci sta, viene tagliato in silenzio finché ci sta. Sorprende molte persone, quindi fai attenzione:
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!("ci sta: {small}"), Err(e) => println!("300 non ci sta in un u8: {e}"), }
match u8::try_from(200) { Ok(small) => println!("ci sta: {small}"), Err(e) => println!("200 non ci sta in un u8: {e}"), }}Output:
300 as u8 = 44-1 as u32 = 4294967295300 non ci sta in un u8: out of range integral type conversion attemptedci sta: 200Il testo dell’errore («out of range integral type conversion attempted», cioè tentata una conversione intera fuori intervallo) viene dalla libreria standard ed è in inglese. 300 as u8 tiene solo gli 8 bit più bassi, cioè 44. -1 as u32 diventa il u32 più grande. Nessuno dei due ti avvisa. u8::try_from controlla prima e ti dà un Result, così puoi decidere cosa fare. Una buona abitudine: usa as solo quando sai che il valore ci sta, e try_from quando il valore arriva da fuori del tuo programma. (I float si comportano diversamente: 300.7 as u8 si ferma a 255 invece di fare il giro.) Le pagine Conversione di tipo e Conversioni vanno più a fondo.
Altre sorprese con i numeri
Sezione intitolata “Altre sorprese con i numeri”Qualche altra cosa che fa inciampare, tutta in un posto:
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));}Output:
7 / 2 = 3-7 / 2 = -3i32::MIN.checked_abs() = Nonei32::MIN.unsigned_abs() = 214748364810u32.checked_pow(9) = Some(1000000000)10u32.checked_pow(10) = None- La divisione intera scarta il resto. Non arrotonda; taglia verso lo zero, quindi
-7 / 2fa-3. Usa%per il resto, o i float se vuoi i decimali. abs()può andare in overflow. Uni32scende fino a -2147483648 ma sale solo fino a 2147483647, quindi la versione positiva dii32::MINnon ci sta.i32::MIN.abs()va in panic nelle build di debug conattempt to negate with overflow. Usachecked_abs(), oppureunsigned_abs(), che restituisce unu32in cui il valore ci sta.powcresce in fretta. 10 alla decima è già troppo grande per unu32. Esistono anchechecked_pow,saturating_powewrapping_pow.
Se un valore deve sempre fare il giro o sempre saturare, puoi avvolgerlo in std::num::Wrapping o std::num::Saturating. Così i normali + e - fanno la cosa giusta, e non puoi dimenticartene:
use std::num::{Saturating, Wrapping};
fn main() { let mut ticket = Wrapping(254u8); ticket += 1; ticket += 1; ticket += 1; println!("biglietto: {ticket}");
let mut battery = Saturating(10u8); battery -= 25; println!("batteria: {battery}");}Output:
biglietto: 1batteria: 0Quale metodo dovrei usare?
Sezione intitolata “Quale metodo dovrei usare?”| Famiglia di metodi | Restituisce | Usala quando |
|---|---|---|
+ - * |
Il numero (panic in debug, fa il giro in release) | Sei sicuro che non possa andare in overflow |
checked_add e simili |
Option<T> |
Un numero sbagliato sarebbe un bug: soldi, dimensioni, ID |
saturating_add e simili |
Il numero, bloccato a MIN o MAX |
C’è un minimo o un massimo naturale: salute, volume, magazzino |
wrapping_add e simili |
Il numero, dopo il giro | Fare il giro è lo scopo: hash, contatori ciclici |
overflowing_add e simili |
(T, bool) |
Vuoi il valore dopo il giro e anche sapere che c’è stato |
T::try_from(x) |
Result<T, _> |
Converti un valore che potrebbe non stare in un tipo più piccolo |
Errori comuni
Sezione intitolata “Errori comuni”Sottrarre 1 dalla lunghezza di una lista vuota
Sezione intitolata “Sottrarre 1 dalla lunghezza di una lista vuota”len() restituisce un usize, che non può essere negativo. Su una lista vuota, len() - 1 farebbe -1, quindi va in overflow:
fn last_index(tasks: &[String]) -> usize { tasks.len() - 1}
fn main() { let tasks: Vec<String> = Vec::new(); println!("l'ultima attività è all'indice {}", last_index(&tasks));}Quando lo esegui, ottieni:
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 backtraceQuesta volta è un «tentativo di sottrazione con overflow». Ci cascano quasi tutti almeno una volta. La soluzione è lasciare che sia il tipo a dirti che la lista potrebbe essere vuota: restituisci tasks.len().checked_sub(1), che è un Option<usize>. Se vuoi solo l’ultimo elemento, tasks.last() è ancora più semplice.
Sommare numeri piccoli in un tipo piccolo
Sezione intitolata “Sommare numeri piccoli in un tipo piccolo”Tre voti da 90, 85 e 100 stanno ciascuno in un u8. Il loro totale, 275, no. Quindi let total: u8 = scores.iter().sum(); va in panic con attempt to add with overflow. Il panic punta dentro la libreria standard, e questo può confondere, ma la causa è il tuo u8. Converti ogni valore in un tipo più grande prima di sommare:
fn main() { let scores: Vec<u8> = vec![90, 85, 100]; let total: u32 = scores.iter().map(|&s| s as u32).sum(); println!("totale: {total}, media: {}", total / 3);}Output:
totale: 275, media: 91Passare da u8 a u32 con as è sempre sicuro, perché ogni u8 sta in un u32.
Pratica
Sezione intitolata “Pratica”1. La batteria del telefono
Sezione intitolata “1. La batteria del telefono”Il livello della batteria di un telefono è un u8 da 0 a 100. Scrivi charge(level: u8, amount: u8) -> u8 e drain(level: u8, amount: u8) -> u8 in modo che non vadano mai sopra 100 o sotto 0. Parti da 80, ricarica di 30, poi scarica di 130, e stampa il livello dopo ogni passo. Suggerimento: saturating_add più .min(100) per ricaricare, saturating_sub per scaricare.
Mostra la soluzione
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!("inizio: {level}%");
level = charge(level, 30); println!("dopo la ricarica: {level}%");
level = drain(level, 130); println!("dopo una lunga giornata: {level}%");}Output:
inizio: 80%dopo la ricarica: 100%dopo una lunga giornata: 0%saturating_sub si ferma a 0 da solo. Per la ricarica, il limite del tipo è 255, quindi .min(100) aggiunge sopra il tuo limite.
2. Un fattoriale che conosce i suoi limiti
Sezione intitolata “2. Un fattoriale che conosce i suoi limiti”Scrivi fn factorial(n: u64) -> Option<u64> che moltiplica 1 × 2 × … × n e restituisce None se il risultato diventa troppo grande per un u64. Stampa il risultato per 5, 20 e 21. Suggerimento: parti da let mut result: u64 = 1; e usa result.checked_mul(i)? dentro un ciclo for.
Mostra la soluzione
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}! è troppo grande per un u64"), } }}Output:
5! = 12020! = 243290200817664000021! è troppo grande per un u64Il ? fa restituire None all’intera funzione nel momento in cui una moltiplicazione non ci sta. 20! è il fattoriale più grande che un u64 riesce a contenere.
Riepilogo
Sezione intitolata “Riepilogo”- Ogni tipo intero ha un intervallo fisso, da
T::MINaT::MAX. Uscirne è un overflow. - Le build di debug vanno in panic con
attempt to add with overflow. Le build di release fanno il giro in silenzio. Un overflow che il compilatore vede è un errore di compilazione. - Scegli tu il comportamento:
checked_dà unOption,saturating_si ferma al limite,wrapping_fa il giro,overflowing_dà il valore e unbool. asnon controlla mai e può cambiare un numero in silenzio. UsaT::try_fromquando un valore potrebbe non starci.- Attenzione a
len() - 1sulle liste vuote, alle somme in tipi piccoli, adabs()diMINe apow.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Cosa succede a 250u8 + 10 in una build di debug, quando i valori arrivano da variabili?
2. Cosa restituisce 100u64.checked_sub(250)?
3. Una barra della salute non deve mai scendere sotto 0. Quale metodo è il più adatto?
4. Quanto vale 300_i32 as u8?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Integer Overflow — The Rust Book (in inglese): la spiegazione ufficiale dei panic in debug, del giro in release e delle quattro famiglie di metodi.
u8— documentazione della libreria standard (in inglese): tutti i metodichecked_,saturating_,wrapping_eoverflowing_, con un piccolo esempio per ciascuno.i32— documentazione della libreria standard (in inglese): gli stessi metodi per i numeri con segno, piùchecked_abs,unsigned_absechecked_pow.- Overflow — The Rust Reference (in inglese): le regole esatte su quali operazioni possono andare in overflow e cosa succede quando lo fanno.
- Casting — Rust by Example (in inglese): esempi eseguibili di cast con
asche fanno il giro, troncano e saturano.