wandres.dev
VARIABLES Y TIPOS · let, mut, tipos escalares

Los tipos escalares y el desbordamiento

Enteros con y sin signo (i8..i128, u8..u128, isize/usize), floats IEEE-754 (f32/f64), bool y char Unicode, y la semántica del desbordamiento: panic en debug, wrap en release, y las familias checked/wrapping/saturating.

⏱ 18 min

Un tipo escalar representa un único valor. Rust tiene cuatro familias: enteros, coma flotante, booleanos y caracteres. La diferencia con lenguajes más laxos no está en la lista —es la de siempre— sino en el rigor: cada ancho es fijo y explícito, no hay conversiones implícitas, y el desbordamiento tiene una semántica definida y comprobable en lugar de ser comportamiento indefinido.

🎯 Al terminar esta lección sabrás
  • Conocer los enteros con y sin signo y los tipos dependientes de plataforma.
  • Entender los floats IEEE-754 y las trampas de bool y char.
  • Dominar la semántica del desbordamiento en debug y release.
  • Elegir entre checked_*, wrapping_*, saturating_* y overflowing_*.

Enteros: ancho y signo explícitos

Rust nombra cada entero por su tamaño en bits y su signo. No hay ambigüedad de plataforma como el int de C:

let a: i8   = -128;                 // con signo, 8 bits:  -128..=127
let b: u8   = 255;                  // sin signo, 8 bits:  0..=255
let c: i32  = -2_147_483_648;       // el default de los enteros
let d: u64  = 18_446_744_073_709_551_615;
let e: i128 = 170_141_183_460_469_231_731_687_303_715_884_105_727;

Los anchos van de 8 a 128 bits: i8/u8, i16/u16, i32/u32, i64/u64, i128/u128. Los con signo usan complemento a dos, así que i8 cubre -128..=127 (asimétrico: un negativo más que positivos). Aparte están isize y usize, cuyo ancho es el de un puntero de la máquina (64 bits en un objetivo de 64 bits): usize es el tipo canónico para indexar y para tamaños de colecciones, precisamente porque debe poder direccionar toda la memoria.

let hex  = 0xff;        // 255 en hexadecimal
let oct  = 0o77;        // 63 en octal
let bin  = 0b1010_1010; // 170 en binario, con _ de separación visual
let byte = b'A';        // literal de byte: un u8 con valor 65

Floats, bool y char

La coma flotante sigue IEEE-754: f32 (simple) y f64 (doble, el default). El default es f64 porque su precisión extra rara vez cuesta rendimiento en hardware moderno.

let x = 2.0;            // f64 por defecto
let y: f32 = 3.14;     // simple precisión, explícita
let inf = f64::INFINITY;
let nan = f64::NAN;
assert!(nan != nan);    // NaN nunca es igual a sí mismo (¡ni a NaN!)

bool ocupa 1 byte y solo vale true o false; no es un entero disfrazado y no se convierte implícitamente a 0/1. char es lo más sorprendente: 4 bytes y representa un Unicode Scalar Value, no un byte ASCII. Puede contener cualquier carácter Unicode salvo los code points sustitutos:

let letra: char = 'z';
let acento: char = 'ñ';
let emoji: char = '🦀';   // un solo char, aunque en UTF-8 ocupe 4 bytes
let corazon: char = '\u{2764}';
⚠️
char no es u8, y una cadena no es un array de char

Un char mide 4 bytes porque debe alojar cualquier escalar Unicode (hasta U+10FFFF). En cambio, un String se almacena en UTF-8, donde cada carácter ocupa de 1 a 4 bytes. Por eso no puedes indexar una cadena con s[0]: no existe un “byte cero que sea el primer carácter” de forma general. Esta distinción —char de 4 bytes frente a texto UTF-8 de ancho variable— es una fuente clásica de confusión que verás a fondo al estudiar cadenas.

El desbordamiento tiene semántica, no caos

En C, desbordar un entero con signo es comportamiento indefinido: el compilador puede asumir que no ocurre y optimizar en consecuencia. Rust cierra ese agujero con una regla dual y definida:

  • En compilación debug, un desbordamiento provoca un panic en tiempo de ejecución. El bug se manifiesta de inmediato, no se propaga en silencio.
  • En compilación release (con overflow-checks desactivado por defecto), la operación hace wrap-around en complemento a dos, de forma determinista.
fn main() {
    let x: u8 = 255;
    let y = x + 1;   // debug: panic 'attempt to add with overflow'
                     // release: y == 0 (envuelve)
    println!("{y}");
}

Depender del wrap silencioso es un bug latente: el mismo código se comporta distinto según el perfil de compilación. Por eso Rust no te deja confiar en el default; te obliga a decir qué quieres mediante métodos explícitos.

flowchart TD
A[Operacion aritmetica] --> B{Se desborda}
B -->|No| Z[Resultado normal]
B -->|Si| C{Perfil de compilacion}
C -->|debug| D[Panic inmediato]
C -->|release| E[Wrap en complemento a dos]

Las cuatro familias explícitas

Cuando el desbordamiento es una posibilidad real y no un bug, no dejes que dependa del perfil: declara la política con un método. Toda operación aritmética tiene cuatro variantes:

let a: u8 = 200;

let r1 = a.checked_add(100);     // Option: None si desborda -> Some/None
let r2 = a.wrapping_add(100);    // envuelve siempre: 200+100 -> 44
let r3 = a.saturating_add(100);  // se queda en el límite: -> 255
let (r4, hubo) = a.overflowing_add(100); // (44, true): valor + flag

Cada familia codifica una intención distinta:

  • checked_* devuelve Option<T>: None si desborda. La opción por defecto cuando un desbordamiento es un error que debes manejar.
  • wrapping_* envuelve con aritmética modular. Correcta y deseada en hashing, criptografía o contadores cíclicos.
  • saturating_* satura en MIN/MAX. Ideal para magnitudes físicas donde “más allá del máximo” debe seguir siendo el máximo (volumen, brillo).
  • overflowing_* devuelve (T, bool): el resultado envuelto más un booleano que indica si hubo desbordamiento.
// Contador que satura en lugar de envolver a 0:
let volumen: u8 = 250;
let nuevo = volumen.saturating_add(20);  // 255, no 14
assert_eq!(nuevo, 255);

Ten cuidado también con as: el casting entre tipos numéricos trunca sin avisar (300u32 as u8 da 44). Es explícito pero sin red; para conversiones seguras entre anchos usarás From/TryFrom más adelante.

Definir el desbordamiento es convertir una clase entera de bugs en una decisión

El desbordamiento entero ha causado desde fallos de cohetes hasta vulnerabilidades de seguridad, y en C sigue siendo comportamiento indefinido: el terreno más resbaladizo del lenguaje. La jugada de Rust es doble y profunda. Primero, elimina el comportamiento indefinido: en debug el programa peta ruidosamente donde está el fallo, y en release el resultado es determinista, nunca “lo que el optimizador decidiera”. Segundo, y más importante, convierte cada suma peligrosa en un lugar donde debes elegir una política: ¿es esto un error (checked), un contador modular (wrapping), una magnitud que satura (saturating), o necesito el flag (overflowing)? El lenguaje no adivina por ti ni te castiga con un crash sorpresa en producción; te fuerza a hacer explícito, en el código, un razonamiento que en otros lenguajes vive solo —cuando vive— en la cabeza del programador. Eso es sustituir esperanza por prueba.

⚔️ Domina los enteros
  1. Provoca un panic de desbordamiento con u8 = 255 más 1 en modo debug; compila luego en --release y observa el wrap.
  2. Reescribe esa suma con las cuatro familias y compara los resultados: checked, wrapping, saturating, overflowing.
  3. Comprueba assert!(f64::NAN != f64::NAN) y razona por qué el orden de los floats es solo parcial.
  4. Imprime sizeof mental: cuántos bytes ocupan char, bool, u8 y i128. Verifícalo con std::mem::size_of.
  5. Trunca 300u32 as u8 y explica el 44 resultante en términos de módulo 256.