Overflow de inteiros em Rust
Todo tipo inteiro em Rust tem um tamanho fixo. Um u8 tem espaço para os números de 0 a 255, e mais nada. Então o que acontece quando você tem 255 e soma 1? A resposta não cabe. Isso se chama integer overflow (estouro de inteiro), ou simplesmente overflow.
Pense no hodômetro de um carro antigo que só tem três dígitos. Depois de 999 quilômetros, ele volta para 000. Isso não tem problema num brinquedo, mas é péssimo no saldo de uma conta bancária. O Rust te ajuda aqui de duas formas. Ele pega o problema fazendo barulho enquanto você está desenvolvendo. E te dá métodos simples para dizer exatamente o que deve acontecer no lugar. Não se preocupe: são só quatro, e todos seguem o mesmo padrão. Esta página parte do que você viu em Tipos de dados e Operadores.
Qual é o tamanho de cada tipo inteiro?
Seção intitulada “Qual é o tamanho de cada tipo inteiro?”Cada tipo tem um menor valor (MIN) e um maior valor (MAX). Você pode imprimi-los com constantes como u8::MAX ou i32::MIN:
fn main() { println!("u8: {} a {}", u8::MIN, u8::MAX); println!("i8: {} a {}", i8::MIN, i8::MAX); println!("u16: {} a {}", u16::MIN, u16::MAX); println!("i32: {} a {}", i32::MIN, i32::MAX); println!("u32: {} a {}", u32::MIN, u32::MAX); println!("i64: {} a {}", i64::MIN, i64::MAX); println!("u64: {} a {}", u64::MIN, u64::MAX); println!("u128: {} a {}", u128::MIN, u128::MAX);}Saída:
u8: 0 a 255i8: -128 a 127u16: 0 a 65535i32: -2147483648 a 2147483647u32: 0 a 4294967295i64: -9223372036854775808 a 9223372036854775807u64: 0 a 18446744073709551615u128: 0 a 340282366920938463463374607431768211455Aqui está a família inteira em uma tabela:
| Tipo | Bits | Menor valor | Maior valor |
|---|---|---|---|
i8 / u8 |
8 | -128 / 0 | 127 / 255 |
i16 / u16 |
16 | -32.768 / 0 | 32.767 / 65.535 |
i32 / u32 |
32 | cerca de -2,1 bilhões / 0 | cerca de 2,1 bilhões / 4,3 bilhões |
i64 / u64 |
64 | cerca de -9,2 × 10^18 / 0 | cerca de 9,2 × 10^18 / 1,8 × 10^19 |
i128 / u128 |
128 | cerca de -1,7 × 10^38 / 0 | cerca de 1,7 × 10^38 / 3,4 × 10^38 |
isize / usize |
64 na maioria dos computadores | igual ao i64 / 0 |
igual ao i64 / u64 |
Um tipo com sinal (i) gasta metade da sua faixa com números negativos. Um tipo sem sinal (u) começa em 0, então o seu maior valor é cerca de duas vezes maior. Qualquer coisa fora dessas faixas é overflow, e ficar abaixo de MIN (como 0 - 1 em um u8) também conta. Esse caso às vezes é chamado de underflow, mas o Rust trata do mesmo jeito.
O que o Rust faz quando um número estoura
Seção intitulada “O que o Rust faz quando um número estoura”Em builds de debug, ele dá panic
Seção intitulada “Em builds de debug, ele dá panic”Quando você roda o programa com cargo run (um build de debug), o Rust confere cada +, - e *. Se a resposta não cabe, o programa para com uma mensagem clara. Aqui um jogo soma pontos de bônus a uma pontuação u8:
fn add_points(score: u8, bonus: u8) -> u8 { score + bonus}
fn main() { let total = add_points(250, 10); println!("total: {total}");}Quando você roda, aparece:
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 backtraceA mensagem attempt to add with overflow quer dizer “tentativa de somar com overflow”, e a linha de cima mostra o arquivo e a linha exata. Os valores passam por parâmetros de função de propósito. Se você escrevesse os números bem do lado do +, o compilador perceberia o problema antes mesmo de o programa rodar (você vai ver isso daqui a pouco). Um panic (parada do programa) pode parecer duro, mas é uma gentileza: você encontra o bug na sua própria máquina, com o número exato da linha, em vez de receber uma pontuação errada em produção. Tem mais sobre panics na página de Panic.
Em builds de release, ele dá a volta
Seção intitulada “Em builds de release, ele dá a volta”Quando você compila com cargo run --release, essas verificações são desligadas para deixar o código um pouco mais rápido. O número dá a volta como o hodômetro: 250 + 10 em um u8 vira 4 (porque 260 - 256 = 4).
cargo run --releasetotal: 4Nada de panic, nada de aviso, só um número errado em silêncio. É por isso que você não deve contar com um overflow que acontece “sem querer”. Se você quer que o número dê a volta, diga isso com wrapping_add (mostrado mais abaixo). Os botões “Try it” deste site rodam builds de debug, então o wrapping_add também é o jeito de ver esse comportamento por lá:
fn main() { let score: u8 = 250; println!("{}", score.wrapping_add(10));}Saída:
4Se você quiser que os builds de release também deem panic, pode religar as verificações no Cargo.toml:
[profile.release]overflow-checks = trueCom números escritos direto no código, nem compila
Seção intitulada “Com números escritos direto no código, nem compila”Quando o compilador consegue ver a conta inteira, ele se recusa a compilar:
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 defaultO compilador avisa que esta operação aritmética vai estourar. O mesmo vale para um literal que é simplesmente grande demais, como let level: u8 = 256;, que dá literal out of range for u8 (“literal fora da faixa de u8”).
Escolhendo o comportamento você mesmo
Seção intitulada “Escolhendo o comportamento você mesmo”Em vez de torcer para o overflow nunca acontecer, você pode escolher o que deve acontecer. Todo tipo inteiro tem quatro famílias de métodos para isso. Todas funcionam do mesmo jeito em u8, i32, u64 e nos outros.
checked_: “me avise se não coube”
Seção intitulada “checked_: “me avise se não coube””checked_add, checked_sub e checked_mul retornam um Option: Some(resposta) se couber, None se não couber. Essa é a escolha mais segura sempre que um número errado seria um problema de verdade, como com dinheiro. O saldo de uma conta não pode ficar abaixo de 0, então um u64 com checked_sub recusa o saque por você:
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!("Saque de {amount} feito, o saldo agora é {left}"), None => println!("Não dá para sacar {amount}: saldo insuficiente"), } }
println!("{:?}", order_total(20, 3, 5)); println!("{:?}", order_total(2_000_000_000, 3, 5));}Saída:
Saque de 30 feito, o saldo agora é 70Não dá para sacar 250: saldo insuficienteSome(65)NoneComo o resultado é um Option, o operador ? também funciona. Em order_total, se a multiplicação estourar, a função retorna None na hora.
saturating_: “pare na borda”
Seção intitulada “saturating_: “pare na borda””saturating_add e saturating_sub nunca passam de MIN ou MAX. Eles simplesmente param ali, como um copo que já está cheio. Isso é perfeito para coisas que têm um piso ou um teto natural: uma barra de vida, um botão de volume, itens no estoque.
fn main() { let health: u8 = 20; let after_hit = health.saturating_sub(35); println!("vida depois de um golpe forte: {after_hit}");
let volume: u8 = 90; let louder = volume.saturating_add(25).min(100); println!("volume depois de aumentar: {louder}");
let stock: u32 = 3; let left = stock.saturating_sub(5); println!("itens restantes no estoque: {left}");}Saída:
vida depois de um golpe forte: 0volume depois de aumentar: 100itens restantes no estoque: 0A linha do volume precisa de um passo a mais. O saturating_add só para no limite do próprio tipo, que é 255 para um u8. O seu botão para em 100, então o .min(100) prende o valor no seu próprio limite.
wrapping_: “dê a volta de propósito”
Seção intitulada “wrapping_: “dê a volta de propósito””Às vezes dar a volta é exatamente o que você quer. Um contador de senhas que vai 254, 255, 0, 1. Ou uma função de hash simples, em que os números devem mesmo ficar embaralhados. wrapping_add e wrapping_mul fazem isso em qualquer build, de debug ou de 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!("senha {ticket}"); ticket = ticket.wrapping_add(1); }
println!("hash de \"hello\": {}", simple_hash("hello")); println!("hash de \"a much longer sentence\": {}", simple_hash("a much longer sentence"));}Saída:
senha 254senha 255senha 0senha 1hash de "hello": 261238937hash de "a much longer sentence": 3373490575Os textos "hello" e "a much longer sentence" ficaram em inglês de propósito: o hash é calculado a partir dos bytes de cada texto, e letras acentuadas ocupam 2 bytes em UTF-8, então outro texto daria outros números. A frase longa estouraria um u32 muitas e muitas vezes com * e + comuns. Com os métodos wrapping_, tudo bem: é justamente isso que o hash deve fazer.
overflowing_: “me dê os dois”
Seção intitulada “overflowing_: “me dê os dois””overflowing_add retorna uma tupla: a resposta que deu a volta e um bool que diz se houve overflow. É útil quando você quer o valor que deu a volta, mas também quer saber que isso aconteceu:
fn main() { let (value, overflowed) = 250u8.overflowing_add(10); println!("{value}, estourou: {overflowed}");
let (value, overflowed) = 50u8.overflowing_add(10); println!("{value}, estourou: {overflowed}");}Saída:
4, estourou: true60, estourou: falseVocê vai usar este bem menos do que os outros. Se não tiver certeza de qual escolher, o checked_ é um ótimo padrão.
Overflow ao converter com as
Seção intitulada “Overflow ao converter com as”Converter entre tipos com as nunca dá panic, nem em builds de debug. Se o valor não cabe, ele é cortado em silêncio até caber. Isso surpreende muita gente, então tome cuidado:
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!("cabe: {small}"), Err(e) => println!("300 não cabe em um u8: {e}"), }
match u8::try_from(200) { Ok(small) => println!("cabe: {small}"), Err(e) => println!("200 não cabe em um u8: {e}"), }}Saída:
300 as u8 = 44-1 as u32 = 4294967295300 não cabe em um u8: out of range integral type conversion attemptedcabe: 200A mensagem out of range integral type conversion attempted vem do próprio Rust, por isso aparece em inglês: ela quer dizer “tentativa de conversão de inteiro fora da faixa”. 300 as u8 guarda só os 8 bits mais baixos, o que dá 44. -1 as u32 vira o maior u32. Nenhum dos dois te avisa. O u8::try_from confere antes e te dá um Result, para você decidir o que fazer. Um bom hábito: use as só quando você sabe que o valor cabe, e try_from quando ele vem de fora do seu programa. (Números de ponto flutuante se comportam diferente: 300.7 as u8 para em 255 em vez de dar a volta.) As páginas de Conversão de tipos e Conversões se aprofundam no assunto.
Outras surpresas com números
Seção intitulada “Outras surpresas com números”Mais algumas coisas que pegam as pessoas desprevenidas, todas num lugar só:
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));}Saída:
7 / 2 = 3-7 / 2 = -3i32::MIN.checked_abs() = Nonei32::MIN.unsigned_abs() = 214748364810u32.checked_pow(9) = Some(1000000000)10u32.checked_pow(10) = None- A divisão de inteiros descarta o resto. Ela não arredonda; ela corta em direção ao zero, então
-7 / 2dá-3. Use%para pegar o resto, ou números de ponto flutuante se quiser casas decimais. - O
abs()pode estourar. Umi32vai até -2147483648 para baixo, mas só até 2147483647 para cima, então a versão positiva dei32::MINnão cabe.i32::MIN.abs()dá panic em builds de debug comattempt to negate with overflow(“tentativa de inverter o sinal com overflow”). Usechecked_abs()ouunsigned_abs(), que retorna umu32, onde o valor cabe. - O
powcresce rápido. 10 elevado a 10 já é grande demais para umu32. Também existemchecked_pow,saturating_powewrapping_pow.
Se um valor deve sempre dar a volta ou sempre saturar, você pode embrulhá-lo em std::num::Wrapping ou std::num::Saturating. Aí o + e o - comuns fazem a coisa certa, e você não tem como esquecer:
use std::num::{Saturating, Wrapping};
fn main() { let mut ticket = Wrapping(254u8); ticket += 1; ticket += 1; ticket += 1; println!("senha: {ticket}");
let mut battery = Saturating(10u8); battery -= 25; println!("bateria: {battery}");}Saída:
senha: 1bateria: 0Qual método devo usar?
Seção intitulada “Qual método devo usar?”| Família de métodos | Retorna | Use quando |
|---|---|---|
+ - * |
O número (panic em debug, dá a volta em release) | Você tem certeza de que não pode estourar |
checked_add e companhia |
Option<T> |
Um número errado seria um bug: dinheiro, tamanhos, IDs |
saturating_add e companhia |
O número, preso em MIN ou MAX |
Existe um piso ou teto natural: vida, volume, estoque |
wrapping_add e companhia |
O número, depois de dar a volta | Dar a volta é a ideia: hashes, contadores que recomeçam |
overflowing_add e companhia |
(T, bool) |
Você quer o valor que deu a volta e saber que isso aconteceu |
T::try_from(x) |
Result<T, _> |
Converter um valor que pode não caber em um tipo menor |
Erros comuns
Seção intitulada “Erros comuns”Subtrair 1 do tamanho de uma lista vazia
Seção intitulada “Subtrair 1 do tamanho de uma lista vazia”len() retorna um usize, que não pode ser negativo. Em uma lista vazia, len() - 1 daria -1, então estoura:
fn last_index(tasks: &[String]) -> usize { tasks.len() - 1}
fn main() { let tasks: Vec<String> = Vec::new(); println!("a última tarefa está no índice {}", last_index(&tasks));}Quando você roda, aparece:
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 backtraceAqui o panic diz “tentativa de subtrair com overflow”. Esse erro pega quase todo mundo pelo menos uma vez. A correção é deixar o tipo te avisar que a lista pode estar vazia: retorne tasks.len().checked_sub(1), que é um Option<usize>. Se você só quer o último item, tasks.last() é ainda mais simples.
Somar números pequenos em um tipo pequeno
Seção intitulada “Somar números pequenos em um tipo pequeno”Três notas de prova, 90, 85 e 100, cabem cada uma em um u8. O total delas, 275, não cabe. Então let total: u8 = scores.iter().sum(); dá panic com attempt to add with overflow. O panic aponta para dentro da biblioteca padrão, o que pode confundir, mas a causa é o seu u8. Converta cada valor para um tipo maior antes de somar:
fn main() { let scores: Vec<u8> = vec![90, 85, 100]; let total: u32 = scores.iter().map(|&s| s as u32).sum(); println!("total: {total}, média: {}", total / 3);}Saída:
total: 275, média: 91Ir de u8 para u32 com as é sempre seguro, porque todo u8 cabe em um u32.
Prática
Seção intitulada “Prática”1. A bateria do celular
Seção intitulada “1. A bateria do celular”O nível da bateria de um celular é um u8 de 0 a 100. Escreva charge(level: u8, amount: u8) -> u8 e drain(level: u8, amount: u8) -> u8, que nunca passam de 100 nem ficam abaixo de 0. Comece em 80, carregue 30, depois gaste 130, e imprima o nível depois de cada passo. Dica: saturating_add mais .min(100) para carregar, saturating_sub para gastar.
Ver solução
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!("início: {level}%");
level = charge(level, 30); println!("depois de carregar: {level}%");
level = drain(level, 130); println!("depois de um dia longo: {level}%");}Saída:
início: 80%depois de carregar: 100%depois de um dia longo: 0%O saturating_sub para em 0 sozinho. Para carregar, o limite do tipo é 255, então o .min(100) coloca o seu próprio limite por cima.
2. Um fatorial que conhece os seus limites
Seção intitulada “2. Um fatorial que conhece os seus limites”Escreva fn factorial(n: u64) -> Option<u64>, que multiplica 1 × 2 × … × n e retorna None se a resposta ficar grande demais para um u64. Imprima o resultado para 5, 20 e 21. Dica: comece com let mut result: u64 = 1; e use result.checked_mul(i)? dentro de um laço for.
Ver solução
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}! é grande demais para um u64"), } }}Saída:
5! = 12020! = 243290200817664000021! é grande demais para um u64O ? retorna None da função inteira no instante em que uma multiplicação não cabe. 20! é o maior fatorial que um u64 consegue guardar.
- Todo tipo inteiro tem uma faixa fixa, de
T::MINaT::MAX. Sair dela é overflow. - Builds de debug dão panic com
attempt to add with overflow. Builds de release dão a volta em silêncio. Um overflow que o compilador consegue ver é um erro de compilação. - Escolha o comportamento você mesmo:
checked_dá umOption,saturating_para na borda,wrapping_dá a volta,overflowing_dá o valor e umbool. - O
asnunca confere e pode mudar um número em silêncio. UseT::try_fromquando um valor pode não caber. - Cuidado com
len() - 1em listas vazias, somas em tipos pequenos,abs()deMINepow.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. O que acontece com 250u8 + 10 em um build de debug, quando os valores vêm de variáveis?
2. O que 100u64.checked_sub(250) retorna?
3. Uma barra de vida nunca deve ficar abaixo de 0. Qual método se encaixa melhor?
4. Quanto vale 300_i32 as u8?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- Integer Overflow — The Rust Book (em inglês): a explicação oficial sobre o panic em debug, a volta em release e as quatro famílias de métodos.
u8— documentação da biblioteca padrão (em inglês): todos os métodoschecked_,saturating_,wrapping_eoverflowing_, com um exemplo curtinho para cada um.i32— documentação da biblioteca padrão (em inglês): os mesmos métodos para números com sinal, maischecked_abs,unsigned_absechecked_pow.- Overflow — The Rust Reference (em inglês): as regras exatas de quais operações podem estourar e o que acontece quando estouram.
- Casting — Rust by Example (em inglês): exemplos executáveis de conversões com
asque dão a volta, truncam e saturam.