Saltar al contenido

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.

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.

  • 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:

Rust
let a: i8   = -128;                 // con signo, 8 bits:  -128..=127let b: u8   = 255;                  // sin signo, 8 bits:  0..=255let c: i32  = -2_147_483_648;       // el default de los enteroslet 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.

Rust
let hex  = 0xff;        // 255 en hexadecimallet oct  = 0o77;        // 63 en octallet bin  = 0b1010_1010; // 170 en binario, con _ de separación visuallet 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.

Rust
let x = 2.0;            // f64 por defectolet y: f32 = 3.14;     // simple precisión, explícitalet 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:

Rust
let letra: char = 'z';let acento: char = 'ñ';let emoji: char = '🦀';   // un solo char, aunque en UTF-8 ocupe 4 byteslet 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.
Rust
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.

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:

Rust
let a: u8 = 200;let r1 = a.checked_add(100);     // Option: None si desborda -> Some/Nonelet r2 = a.wrapping_add(100);    // envuelve siempre: 200+100 -> 44let r3 = a.saturating_add(100);  // se queda en el límite: -> 255let (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.
Rust
// Contador que satura en lugar de envolver a 0:let volumen: u8 = 250;let nuevo = volumen.saturating_add(20);  // 255, no 14assert_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.