Ir al contenido

Desbordamiento de enteros en Rust

Cada tipo de entero en Rust tiene un tamaño fijo. En un u8 caben los números del 0 al 255, y nada más. Entonces, ¿qué pasa si tienes 255 y le sumas 1? El resultado no cabe. Eso se llama integer overflow (desbordamiento de enteros).

Piensa en el cuentakilómetros de un coche antiguo con solo tres cifras. Después de 999 kilómetros vuelve a 000. Para un juguete no pasa nada, pero para el saldo de una cuenta bancaria es terrible. Rust te ayuda aquí de dos maneras. Detecta el problema a lo grande mientras desarrollas. Y te da métodos sencillos para decir exactamente qué debe pasar en su lugar. Tranquilo, solo son cuatro, y todos siguen el mismo patrón. Esta página parte de lo que viste en Tipos de datos y Operadores.

Cada tipo tiene un valor mínimo (MIN) y un valor máximo (MAX). Puedes imprimirlos con constantes como u8::MAX o i32::MIN:

fn main() {
println!("u8: de {} a {}", u8::MIN, u8::MAX);
println!("i8: de {} a {}", i8::MIN, i8::MAX);
println!("u16: de {} a {}", u16::MIN, u16::MAX);
println!("i32: de {} a {}", i32::MIN, i32::MAX);
println!("u32: de {} a {}", u32::MIN, u32::MAX);
println!("i64: de {} a {}", i64::MIN, i64::MAX);
println!("u64: de {} a {}", u64::MIN, u64::MAX);
println!("u128: de {} a {}", u128::MIN, u128::MAX);
}

Salida:

u8: de 0 a 255
i8: de -128 a 127
u16: de 0 a 65535
i32: de -2147483648 a 2147483647
u32: de 0 a 4294967295
i64: de -9223372036854775808 a 9223372036854775807
u64: de 0 a 18446744073709551615
u128: de 0 a 340282366920938463463374607431768211455
▶ Pruébalo tú mismo

Aquí tienes toda la familia en una tabla:

Tipo Bits Mínimo Máximo
i8 / u8 8 -128 / 0 127 / 255
i16 / u16 16 -32768 / 0 32767 / 65535
i32 / u32 32 unos -2100 millones / 0 unos 2100 millones / 4300 millones
i64 / u64 64 unos -9,2 × 10^18 / 0 unos 9,2 × 10^18 / 1,8 × 10^19
i128 / u128 128 unos -1,7 × 10^38 / 0 unos 1,7 × 10^38 / 3,4 × 10^38
isize / usize 64 en la mayoría de los equipos igual que i64 / 0 igual que i64 / u64

Un tipo con signo (i) dedica la mitad de su rango a los números negativos. Un tipo sin signo (u) empieza en 0, así que su valor máximo es aproximadamente el doble. Cualquier cosa fuera de estos rangos es un desbordamiento, y bajar de MIN (como 0 - 1 en un u8) también cuenta. A veces a eso se le llama underflow, pero Rust lo trata igual.

Qué hace Rust cuando un número se desborda

Sección titulada «Qué hace Rust cuando un número se desborda»

Cuando ejecutas tu programa con cargo run (una compilación debug), Rust comprueba cada +, - y *. Si el resultado no cabe, el programa se detiene con un mensaje claro: un panic. Aquí un juego suma puntos extra a una puntuación de tipo u8:

fn add_points(score: u8, bonus: u8) -> u8 {
score + bonus
}
fn main() {
let total = add_points(250, 10);
println!("total: {total}");
}

Al ejecutarlo, obtienes:

thread 'main' (12345) panicked at src/main.rs:2:5:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ Pruébalo tú mismo

El mensaje attempt to add with overflow significa “intento de sumar con desbordamiento”, y te dice la línea exacta. Los valores pasan por parámetros de función a propósito. Si escribieras los números justo al lado del +, el compilador vería el problema antes incluso de ejecutar el programa (lo verás en un momento). Que el programa se detenga puede parecer duro, pero es un favor: encuentras el bug en tu propia máquina, con el número de línea exacto, en vez de tener una puntuación equivocada en producción. Hay más sobre los panics en la página de Panic.

Cuando compilas con cargo run --release, esas comprobaciones se desactivan para que el código sea un poco más rápido. El número da la vuelta como el cuentakilómetros: 250 + 10 en un u8 se convierte en 4 (porque 260 - 256 = 4).

Terminal window
cargo run --release
total: 4

Ni panic ni aviso, solo un número equivocado en silencio. Por eso no deberías depender de que el desbordamiento ocurra “por accidente”. Si quieres que dé la vuelta, dilo con wrapping_add (lo verás más abajo). Los botones “Pruébalo tú mismo” de este sitio ejecutan compilaciones debug, así que wrapping_add es también la forma de ver este comportamiento ahí:

fn main() {
let score: u8 = 250;
println!("{}", score.wrapping_add(10));
}

Salida:

4
▶ Pruébalo tú mismo

Si quieres que las compilaciones release también hagan panic, puedes volver a activar las comprobaciones en Cargo.toml:

[profile.release]
overflow-checks = true

Con números escritos directamente, ni siquiera compila

Sección titulada «Con números escritos directamente, ni siquiera compila»

Cuando el compilador puede ver el cálculo completo, se niega a 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 default

El error dice que esta operación aritmética se desbordará: u8::MAX + 1 no cabe. Lo mismo pasa con un literal que directamente es demasiado grande, como let level: u8 = 256;, que da literal out of range for u8 (literal fuera de rango para u8).

En lugar de esperar que nunca haya desbordamiento, puedes elegir qué debe pasar. Cada tipo de entero tiene cuatro familias de métodos para esto. Todas funcionan igual en u8, i32, u64 y el resto.

checked_add, checked_sub y checked_mul devuelven un Option: Some(resultado) si cabe, None si no. Es la opción más segura siempre que un número equivocado sería un problema de verdad, como con el dinero. El saldo de una cuenta no puede bajar de 0, así que un u64 con checked_sub rechaza la retirada por ti:

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!("Retiraste {amount}, ahora el saldo es {left}"),
None => println!("No puedes retirar {amount}: no hay suficiente dinero"),
}
}
println!("{:?}", order_total(20, 3, 5));
println!("{:?}", order_total(2_000_000_000, 3, 5));
}

Salida:

Retiraste 30, ahora el saldo es 70
No puedes retirar 250: no hay suficiente dinero
Some(65)
None
▶ Pruébalo tú mismo

Como el resultado es un Option, el operador ? también funciona. En order_total, si la multiplicación se desborda, la función devuelve None de inmediato.

saturating_add y saturating_sub nunca pasan de MIN ni de MAX. Simplemente se quedan ahí, como un vaso que ya está lleno. Es perfecto para cosas que tienen un mínimo o un máximo natural: una barra de vida, un control de volumen, los artículos en stock.

fn main() {
let health: u8 = 20;
let after_hit = health.saturating_sub(35);
println!("vida tras un golpe fuerte: {after_hit}");
let volume: u8 = 90;
let louder = volume.saturating_add(25).min(100);
println!("volumen tras subirlo: {louder}");
let stock: u32 = 3;
let left = stock.saturating_sub(5);
println!("artículos que quedan en stock: {left}");
}

Salida:

vida tras un golpe fuerte: 0
volumen tras subirlo: 100
artículos que quedan en stock: 0
▶ Pruébalo tú mismo

La línea del volumen necesita un paso más. saturating_add solo se detiene en el límite del propio tipo, 255 para un u8. Tu control llega hasta 100, así que .min(100) lo limita a tu propio tope.

A veces dar la vuelta es justo lo que quieres. Un contador de turnos que va 254, 255, 0, 1. O una función hash sencilla, donde se supone que los números se mezclan. wrapping_add y wrapping_mul hacen esto en cualquier compilación, debug o 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!("turno {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"));
}

Salida:

turno 254
turno 255
turno 0
turno 1
hash de "hello": 261238937
hash de "a much longer sentence": 3373490575
▶ Pruébalo tú mismo

Dejamos en inglés los textos "hello" y "a much longer sentence" a propósito: el hash se calcula byte a byte, así que con otro texto saldrían otros números. Con * y + normales, la frase larga desbordaría un u32 muchas veces. Con los métodos wrapping_, no pasa nada: es justo lo que tiene que hacer el hash.

overflowing_add devuelve una tupla: el resultado tras dar la vuelta y un bool que dice si hubo desbordamiento. Es útil cuando quieres el valor que da la vuelta pero también saber que ocurrió:

fn main() {
let (value, overflowed) = 250u8.overflowing_add(10);
println!("{value}, ¿desbordó?: {overflowed}");
let (value, overflowed) = 50u8.overflowing_add(10);
println!("{value}, ¿desbordó?: {overflowed}");
}

Salida:

4, ¿desbordó?: true
60, ¿desbordó?: false
▶ Pruébalo tú mismo

Este lo usarás menos que los demás. Si no sabes cuál elegir, checked_ es una opción por defecto excelente.

Convertir entre tipos con as nunca hace panic, ni siquiera en las compilaciones debug. Si el valor no cabe, se recorta en silencio hasta que quepa. Eso sorprende a mucha gente, así que ten 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 no cabe en un u8: {e}"),
}
match u8::try_from(200) {
Ok(small) => println!("cabe: {small}"),
Err(e) => println!("200 no cabe en un u8: {e}"),
}
}

Salida:

300 as u8 = 44
-1 as u32 = 4294967295
300 no cabe en un u8: out of range integral type conversion attempted
cabe: 200
▶ Pruébalo tú mismo

El texto out of range integral type conversion attempted es el mensaje de error de Rust, que siempre sale en inglés: significa “se intentó una conversión de entero fuera de rango”. 300 as u8 se queda solo con los 8 bits más bajos, que valen 44. -1 as u32 se convierte en el u32 más grande. Ninguno de los dos te avisa. u8::try_from comprueba primero y te da un Result, así que tú decides qué hacer. Un buen hábito: usa as solo cuando sepas que el valor cabe, y try_from cuando venga de fuera de tu programa. (Los números de punto flotante se comportan distinto: 300.7 as u8 se queda en 255 en vez de dar la vuelta). Las páginas de Conversión de tipos y Conversiones entran en más detalle.

Unas cuantas cosas más que confunden a la gente, todas en un mismo sitio:

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));
}

Salida:

7 / 2 = 3
-7 / 2 = -3
i32::MIN.checked_abs() = None
i32::MIN.unsigned_abs() = 2147483648
10u32.checked_pow(9) = Some(1000000000)
10u32.checked_pow(10) = None
▶ Pruébalo tú mismo
  • La división entera descarta el resto. No redondea; corta hacia el cero, así que -7 / 2 es -3. Usa % para obtener el resto, o números de punto flotante si quieres decimales.
  • abs() puede desbordarse. Un i32 baja hasta -2147483648 pero solo sube hasta 2147483647, así que la versión positiva de i32::MIN no cabe. i32::MIN.abs() hace panic en las compilaciones debug con attempt to negate with overflow (intento de cambiar el signo con desbordamiento). Usa checked_abs(), o unsigned_abs(), que devuelve un u32, donde sí cabe.
  • pow crece muy rápido. 10 elevado a 10 ya es demasiado grande para un u32. También existen checked_pow, saturating_pow y wrapping_pow.

Si un valor tiene que dar la vuelta siempre o saturarse siempre, puedes envolverlo en std::num::Wrapping o std::num::Saturating. Así, + y - normales hacen lo correcto, y no se te puede olvidar:

use std::num::{Saturating, Wrapping};
fn main() {
let mut ticket = Wrapping(254u8);
ticket += 1;
ticket += 1;
ticket += 1;
println!("turno: {ticket}");
let mut battery = Saturating(10u8);
battery -= 25;
println!("batería: {battery}");
}

Salida:

turno: 1
batería: 0
▶ Pruébalo tú mismo
Familia de métodos Devuelve Úsala cuando
+ - * El número (panic en debug, da la vuelta en release) Estás seguro de que no puede desbordarse
checked_add y compañía Option<T> Un número equivocado sería un bug: dinero, tamaños, IDs
saturating_add y compañía El número, detenido en MIN o MAX Hay un mínimo o máximo natural: vida, volumen, stock
wrapping_add y compañía El número, tras dar la vuelta Dar la vuelta es justo lo que buscas: hashes, contadores cíclicos
overflowing_add y compañía (T, bool) Quieres el valor tras dar la vuelta y saber si la dio
T::try_from(x) Result<T, _> Conviertes un valor que podría no caber en un tipo más pequeño

len() devuelve un usize, que no puede ser negativo. En una lista vacía, len() - 1 sería -1, así que se desborda:

fn last_index(tasks: &[String]) -> usize {
tasks.len() - 1
}
fn main() {
let tasks: Vec<String> = Vec::new();
println!("la última tarea está en el índice {}", last_index(&tasks));
}

Al ejecutarlo, obtienes:

thread 'main' (12345) panicked at src/main.rs:2:5:
attempt to subtract with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ Pruébalo tú mismo

attempt to subtract with overflow significa “intento de restar con desbordamiento”. A casi todo el mundo le pasa esto al menos una vez. La solución es dejar que el tipo te diga que la lista puede estar vacía: devuelve tasks.len().checked_sub(1), que es un Option<usize>. Si solo quieres el último elemento, tasks.last() es todavía más sencillo.

Sumar números pequeños en un tipo pequeño

Sección titulada «Sumar números pequeños en un tipo pequeño»

Tres notas de examen de 90, 85 y 100 caben cada una en un u8. Su total, 275, no. Así que let total: u8 = scores.iter().sum(); hace panic con attempt to add with overflow. El panic apunta a un sitio dentro de la biblioteca estándar, lo que puede confundir, pero la causa es tu u8. Convierte cada valor a un tipo más grande antes de sumar:

fn main() {
let scores: Vec<u8> = vec![90, 85, 100];
let total: u32 = scores.iter().map(|&s| s as u32).sum();
println!("total: {total}, media: {}", total / 3);
}

Salida:

total: 275, media: 91
▶ Pruébalo tú mismo

Pasar de u8 a u32 con as siempre es seguro, porque cualquier u8 cabe en un u32.

El nivel de batería de un teléfono es un u8 de 0 a 100. Escribe charge(level: u8, amount: u8) -> u8 y drain(level: u8, amount: u8) -> u8, que nunca pasen de 100 ni bajen de 0. Empieza en 80, carga 30, luego gasta 130, e imprime el nivel después de cada paso. Pista: saturating_add más .min(100) para cargar, y saturating_sub para gastar.

Ver solución
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!("inicio: {level}%");
level = charge(level, 30);
println!("tras cargar: {level}%");
level = drain(level, 130);
println!("tras un día largo: {level}%");
}

Salida:

inicio: 80%
tras cargar: 100%
tras un día largo: 0%
▶ Pruébalo tú mismo

saturating_sub se detiene en 0 por sí solo. Para cargar, el límite del tipo es 255, así que .min(100) añade tu propio tope encima.

Escribe fn factorial(n: u64) -> Option<u64>, que multiplique 1 × 2 × … × n y devuelva None si el resultado se hace demasiado grande para un u64. Imprime el resultado para 5, 20 y 21. Pista: empieza con let mut result: u64 = 1; y usa result.checked_mul(i)? dentro de un bucle for.

Ver solución
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}! es demasiado grande para un u64"),
}
}
}

Salida:

5! = 120
20! = 2432902008176640000
21! es demasiado grande para un u64
▶ Pruébalo tú mismo

El ? hace que toda la función devuelva None en cuanto una multiplicación no cabe. 20! es el factorial más grande que cabe en un u64.

  • Cada tipo de entero tiene un rango fijo, de T::MIN a T::MAX. Salirse de él es un desbordamiento.
  • Las compilaciones debug hacen panic con attempt to add with overflow. Las compilaciones release dan la vuelta en silencio. Un desbordamiento que el compilador puede ver es un error de compilación.
  • Elige tú el comportamiento: checked_ da un Option, saturating_ se queda en el borde, wrapping_ da la vuelta, overflowing_ da el valor y un bool.
  • as nunca comprueba nada y puede cambiar un número sin avisar. Usa T::try_from cuando un valor podría no caber.
  • Cuidado con len() - 1 en listas vacías, las sumas en tipos pequeños, el abs() de MIN y pow.

Comprueba lo aprendido

1. ¿Qué pasa con 250u8 + 10 en una compilación debug, cuando los valores vienen de variables?

2. ¿Qué devuelve 100u64.checked_sub(250)?

3. Una barra de vida nunca debe bajar de 0. ¿Qué método encaja mejor?

4. ¿Cuánto vale 300_i32 as u8?

Puntuación: 0 / 4