En esta página

Variables, mutabilidad y tipos primitivos

14 min lectura TextoCap. 1 — Fundamentos de Rust

Variables en Rust: inmutables por defecto

Una de las primeras sorpresas de Rust para desarrolladores que vienen de otros lenguajes es que las variables son inmutables por defecto. Cuando declaras let x = 5, el valor de x no puede cambiar.

Esto no es una limitación — es una característica de diseño deliberada. La inmutabilidad por defecto ayuda al compilador a hacer optimizaciones, facilita el razonamiento sobre el código y previene una categoría completa de bugs donde un valor cambia inesperadamente.

fn main() {
    let x = 5;
    x = 6; // Error: cannot assign twice to immutable variable `x`
}

El compilador de Rust produce un mensaje de error claro:

error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:3:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut x = 5;
  |         +++

Nota cómo el compilador incluso te sugiere la solución.

Variables mutables con `mut`

Cuando necesitas que una variable cambie, añades la palabra clave mut:

fn main() {
    let mut temperatura = 20.0_f64;
    println!("Temperatura inicial: {temperatura}°C");
    
    temperatura += 5.0;
    println!("Temperatura aumentada: {temperatura}°C");
    
    temperatura *= 1.8;
    temperatura += 32.0;
    println!("En Fahrenheit: {temperatura}°F");
}

La mutabilidad es explícita e intencionada. Cuando lees código Rust y ves let mut, sabes inmediatamente que esa variable va a cambiar. Cuando ves solo let, sabes que no va a cambiar — lo cual es información valiosa para entender el flujo del programa.

Constantes con `const`

Las constantes se declaran con const y siempre requieren una anotación de tipo explícita. Además, solo pueden contener valores que se calculan en tiempo de compilación:

// Convención: SCREAMING_SNAKE_CASE para constantes
const VELOCIDAD_DE_LUZ: u64 = 299_792_458; // metros por segundo
const PI: f64 = 3.14159265358979323846;
const NOMBRE_APP: &str = "MiAplicación";

fn main() {
    let radio = 5.0_f64;
    let area = PI * radio * radio;
    println!("Área del círculo: {area:.2}");
}

Las constantes son diferentes a las variables inmutables:

  • Existen para toda la duración del programa (no tienen un scope como let)
  • Pueden declararse en el scope global (fuera de funciones)
  • Su valor debe ser calculable en tiempo de compilación

Shadowing: redeclarar variables

Rust permite redeclarar una variable con let en el mismo scope. La nueva declaración "sombrea" (shadows) la anterior:

fn main() {
    let x = 5;
    
    // El segundo `let x` sombrea al primero
    let x = x + 1;
    
    {
        // Este shadowing solo existe en este bloque
        let x = x * 2;
        println!("x en el bloque interno: {x}"); // 12
    }
    
    println!("x en el scope externo: {x}"); // 6
}

La diferencia crucial entre shadowing y mut es que el shadowing puede cambiar el tipo:

fn main() {
    // Primero es una cadena de texto
    let valor = "42";
    println!("Como texto: {valor}");
    
    // Ahora es un número — mismo nombre, distinto tipo
    let valor: i32 = valor.parse().expect("No es un número");
    println!("Como número: {valor}");
    
    // Ahora es un booleano
    let valor = valor > 0;
    println!("Es positivo: {valor}");
}

Con mut esto sería imposible — no puedes cambiar el tipo de una variable mutable.

Tipos escalares en Rust

Enteros

Rust tiene enteros con signo (i) y sin signo (u) de varios tamaños:

Tipo Rango
i8 -128 a 127
i16 -32,768 a 32,767
i32 -2,147,483,648 a 2,147,483,647 (por defecto)
i64 -9.2 × 10¹⁸ a 9.2 × 10¹⁸
i128 Enorme
isize Dependiente de la arquitectura (32 o 64 bits)
u8 0 a 255
u32 0 a 4,294,967,295
u64 0 a 18.4 × 10¹⁸
usize Para índices y tamaños de colecciones
fn main() {
    let entero_con_signo: i32 = -42;
    let entero_sin_signo: u32 = 42;
    let indice: usize = 0; // Para indexar arrays/vectores
    
    // Literales en distintas bases
    let decimal = 1_000_000;
    let hexadecimal = 0xFF;        // 255
    let octal = 0o77;              // 63
    let binario = 0b1111_0000;     // 240
    let byte: u8 = b'A';           // 65 (código ASCII)
    
    println!("{decimal} {hexadecimal} {octal} {binario} {byte}");
}

Flotantes

fn main() {
    let f32_val: f32 = 3.14;  // 32 bits, menor precisión
    let f64_val: f64 = 3.14159265358979; // 64 bits, por defecto
    
    // Notación científica
    let avogadro = 6.022e23_f64;
    let electron = 1.6e-19_f64;
    
    println!("{f32_val:.2}");
    println!("{f64_val:.10}");
}

Booleanos

fn main() {
    let es_rust_genial: bool = true;
    let tiene_gc: bool = false;
    
    println!("¿Es Rust genial? {es_rust_genial}");
    println!("¿Tiene GC? {tiene_gc}");
    
    // Los booleanos ocupan 1 byte en memoria
    println!("Tamaño de bool: {} byte(s)", std::mem::size_of::<bool>());
}

Caracteres

El tipo char en Rust representa un punto de código Unicode completo (4 bytes), no solo ASCII:

fn main() {
    let letra: char = 'A';
    let emoji: char = '🦀';  // El cangrejo de Rust
    let chino: char = '中';
    let acento: char = 'ñ';
    
    println!("{letra} {emoji} {chino} {acento}");
    println!("Tamaño de char: {} bytes", std::mem::size_of::<char>());
}

&str vs String

Esta distinción es fundamental en Rust y merece atención especial:

&str (string slice): Es una referencia a datos de texto que alguien más posee. El texto suele estar en el binario del programa o en algún String en el heap. Es inmutable y de tamaño conocido en tiempo de compilación (o en tiempo de ejecución).

String: Es un tipo de texto de tamaño dinámico que posee sus datos en el heap. Es mutable y puede crecer o reducirse.

fn main() {
    // &str — string literal, en el segmento de datos del binario
    let saludo: &str = "Hola, Rust";
    
    // String — en el heap, poseído y mutable
    let mut nombre = String::from("Rustáceo");
    nombre.push_str(" el Cangrejo");
    nombre.push('!');
    
    println!("{saludo}, {nombre}");
    
    // Conversiones
    let s: String = saludo.to_string();
    let r: &str = &nombre;  // String -> &str con &
    
    // len() en &str es bytes, no caracteres Unicode
    let texto = "ñoño";
    println!("Bytes: {}", texto.len());        // 6 (ñ ocupa 2 bytes en UTF-8)
    println!("Chars: {}", texto.chars().count()); // 4
}

Inferencia de tipos

Rust tiene inferencia de tipos sofisticada. En la mayoría de casos no necesitas anotar el tipo explícitamente:

fn main() {
    let x = 42;          // i32 por defecto
    let y = 3.14;        // f64 por defecto
    let z = true;        // bool
    let s = "hola";      // &str
    
    // Cuando el contexto lo requiere, anotas el tipo
    let numeros: Vec<i32> = Vec::new();
    let parsed: u64 = "100".parse().unwrap();
    
    // O con turbofish cuando llamas funciones genéricas
    let parsed2 = "100".parse::<u64>().unwrap();
    
    println!("{x} {y} {z} {s} {parsed} {parsed2}");
}

Con un dominio sólido de variables y tipos primitivos, estás listo para explorar funciones y tipos compuestos, que te permitirán estructurar programas más complejos.

Shadowing vs mut
El shadowing con let permite cambiar el tipo de una variable y aplicar transformaciones encadenadas. Con mut solo puedes cambiar el valor, nunca el tipo. Prefiere shadowing cuando la transformación produce un valor conceptualmente distinto.
Separador de miles con guión bajo
Rust permite usar _ como separador visual en literales numéricos: 1_000_000 es igual a 1000000. También funciona en binario: 0b1111_0000, hexadecimal: 0xFF_FF, y octal: 0o77.
rust
fn main() {
    // Por defecto: inmutable
    let x = 5;
    println!("x = {x}");
    // x = 6; // Error de compilación

    // Con mut: mutable
    let mut contador = 0;
    contador += 1;
    contador += 1;
    println!("contador = {contador}"); // 2

    // Shadowing: redeclarar con let
    let y = 10;
    let y = y * 2;    // sombrea la anterior
    let y = y + 3;    // sombrea de nuevo
    println!("y = {y}"); // 23

    // Shadowing permite cambiar el tipo
    let espacios = "   ";         // &str
    let espacios = espacios.len(); // usize
    println!("espacios = {espacios}");

    // Constantes: siempre inmutables, necesitan tipo
    const MAX_PUNTOS: u32 = 100_000;
    println!("máximo = {MAX_PUNTOS}");
}