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.
¿Qué tamaño tiene cada tipo de entero?
Sección titulada «¿Qué tamaño tiene cada tipo de entero?»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 255i8: de -128 a 127u16: de 0 a 65535i32: de -2147483648 a 2147483647u32: de 0 a 4294967295i64: de -9223372036854775808 a 9223372036854775807u64: de 0 a 18446744073709551615u128: de 0 a 340282366920938463463374607431768211455Aquí 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»En las compilaciones debug, hace panic
Sección titulada «En las compilaciones debug, hace panic»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 overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceEl 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.
En las compilaciones release, da la vuelta
Sección titulada «En las compilaciones release, da la vuelta»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).
cargo run --releasetotal: 4Ni 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:
4Si quieres que las compilaciones release también hagan panic, puedes volver a activar las comprobaciones en Cargo.toml:
[profile.release]overflow-checks = trueCon 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 defaultEl 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).
Elegir tú mismo el comportamiento
Sección titulada «Elegir tú mismo el comportamiento»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_: “avísame si no cabe”
Sección titulada «checked_: “avísame si no cabe”»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 70No puedes retirar 250: no hay suficiente dineroSome(65)NoneComo 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_: “quédate en el borde”
Sección titulada «saturating_: “quédate en el borde”»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: 0volumen tras subirlo: 100artículos que quedan en stock: 0La 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.
wrapping_: “da la vuelta a propósito”
Sección titulada «wrapping_: “da la vuelta a propósito”»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 254turno 255turno 0turno 1hash de "hello": 261238937hash de "a much longer sentence": 3373490575Dejamos 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_: “dame las dos cosas”
Sección titulada «overflowing_: “dame las dos cosas”»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ó?: true60, ¿desbordó?: falseEste lo usarás menos que los demás. Si no sabes cuál elegir, checked_ es una opción por defecto excelente.
Desbordamiento al convertir con as
Sección titulada «Desbordamiento al convertir con as»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 = 4294967295300 no cabe en un u8: out of range integral type conversion attemptedcabe: 200El 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.
Otras sorpresas con los números
Sección titulada «Otras sorpresas con los números»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 = -3i32::MIN.checked_abs() = Nonei32::MIN.unsigned_abs() = 214748364810u32.checked_pow(9) = Some(1000000000)10u32.checked_pow(10) = None- La división entera descarta el resto. No redondea; corta hacia el cero, así que
-7 / 2es-3. Usa%para obtener el resto, o números de punto flotante si quieres decimales. abs()puede desbordarse. Uni32baja hasta -2147483648 pero solo sube hasta 2147483647, así que la versión positiva dei32::MINno cabe.i32::MIN.abs()hace panic en las compilaciones debug conattempt to negate with overflow(intento de cambiar el signo con desbordamiento). Usachecked_abs(), ounsigned_abs(), que devuelve unu32, donde sí cabe.powcrece muy rápido. 10 elevado a 10 ya es demasiado grande para unu32. También existenchecked_pow,saturating_powywrapping_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: 1batería: 0¿Qué método debo usar?
Sección titulada «¿Qué método debo usar?»| 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 |
Errores comunes
Sección titulada «Errores comunes»Restar 1 a una longitud vacía
Sección titulada «Restar 1 a una longitud vacía»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 overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceattempt 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: 91Pasar de u8 a u32 con as siempre es seguro, porque cualquier u8 cabe en un u32.
Práctica
Sección titulada «Práctica»1. La batería de un teléfono
Sección titulada «1. La batería de un teléfono»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%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.
2. Un factorial que conoce sus límites
Sección titulada «2. Un factorial que conoce sus límites»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! = 12020! = 243290200817664000021! es demasiado grande para un u64El ? 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.
Resumen
Sección titulada «Resumen»- Cada tipo de entero tiene un rango fijo, de
T::MINaT::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 unOption,saturating_se queda en el borde,wrapping_da la vuelta,overflowing_da el valor y unbool. asnunca comprueba nada y puede cambiar un número sin avisar. UsaT::try_fromcuando un valor podría no caber.- Cuidado con
len() - 1en listas vacías, las sumas en tipos pequeños, elabs()deMINypow.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»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
Para leer más
Sección titulada «Para leer más»- Integer Overflow — El libro de Rust (en inglés): la explicación oficial del panic en debug, la vuelta en release y las cuatro familias de métodos.
u8— documentación de la biblioteca estándar (en inglés): todos los métodoschecked_,saturating_,wrapping_yoverflowing_, con un ejemplo pequeño de cada uno.i32— documentación de la biblioteca estándar (en inglés): los mismos métodos para números con signo, máschecked_abs,unsigned_absychecked_pow.- Overflow — The Rust Reference (en inglés): las reglas exactas de qué operaciones pueden desbordarse y qué pasa cuando lo hacen.
- Casting — Rust by Example (en inglés): ejemplos ejecutables de conversiones con
asque dan la vuelta, recortan y saturan.