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

Shadowing frente a mutabilidad, y las constantes

Redeclarar con let (shadowing) frente a mut como estrategias distintas, y las constantes de Rust: const evaluada en compilación frente a static con dirección fija y vida 'static.

⏱ 16 min

Rust ofrece dos formas de “cambiar” una variable que parecen equivalentes pero significan cosas opuestas: mutar un binding mut o volver a declararlo con let (shadowing). Elegir la correcta comunica intención. Y en el otro extremo del espectro están las constantes —const y static—, valores fijados en compilación cuya semántica de memoria conviene no confundir.

🎯 Al terminar esta lección sabrás
  • Distinguir shadowing (nuevo binding) de mutación (mismo binding).
  • Saber cuándo cada estrategia es la idiomática.
  • Entender const: evaluada en compilación, sin dirección, en línea.
  • Entender static: dirección única, vida 'static, y por qué static mut es unsafe.

Shadowing: un binding nuevo con el mismo nombre

El shadowing consiste en declarar con let una variable cuyo nombre ya existe. No mutas nada: creas un binding completamente nuevo que oculta (shadows) al anterior. La diferencia crucial frente a mut es que el nuevo binding puede tener otro tipo:

fn main() {
    let espacios = "   ";           // &str
    let espacios = espacios.len();  // ahora usize: ¡tipo distinto!
    println!("{espacios}");
}

Esto es imposible con mut, porque mutar exige conservar el tipo:

fn main() {
    let mut espacios = "   ";
    espacios = espacios.len(); // error[E0308]: mismatched types (&str vs usize)
}

El patrón idiomático es transformar y estrechar un valor por etapas, reutilizando el nombre para no inventar entrada_raw, entrada_limpia, entrada_num:

fn main() {
    let entrada = "  42  ";            // &str crudo
    let entrada = entrada.trim();      // &str recortado
    let entrada: i32 = entrada.parse().unwrap(); // i32 final
    println!("{}", entrada + 1);
}
ℹ️
Shadowing respeta el alcance

Como cada let crea un binding nuevo, el shadowing obedece las llaves: dentro de un bloque interior puedes ensombrecer una variable y, al salir, reaparece la original intacta. No has modificado el valor exterior; solo lo tapaste temporalmente. Esto lo hace más seguro que la mutación para transformaciones locales: no puedes “estropear” sin querer el valor de fuera.

Shadowing frente a mut: qué comunica cada uno

Aunque a veces sean intercambiables, transmiten intenciones distintas, y elegir bien es parte de escribir Rust idiomático:

🔁

mut: estado que evoluciona

El mismo lugar cambia de valor en el tiempo, conservando el tipo. Úsalo para acumuladores, contadores y buffers: un dato que es el mismo pero vale distinto a lo largo del programa.

🪆

shadowing: valor derivado

Un valor nuevo se construye a partir del anterior, quizá con otro tipo. Úsalo para pipelines de transformación donde cada etapa produce un dato conceptualmente distinto que ya no necesita al previo.

Regla mental: si te preguntas “¿es el mismo dato en otro momento?” usa mut; si es “¿un dato nuevo hecho a partir del viejo?” usa shadowing. Además, el shadowing te deja mantener el resultado inmutable tras la transformación, algo que mut no ofrece.

const: constantes evaluadas en compilación

Una const es un valor calculado en tiempo de compilación e insertado en línea allí donde se use. No tiene una posición fija en memoria: conceptualmente es un nombre para un valor, no una variable. Requiere anotación de tipo obligatoria y se nombra en SCREAMING_SNAKE_CASE:

const MAX_PUNTOS: u32 = 100_000;
const SEGUNDOS_POR_HORA: u32 = 60 * 60;   // se evalúa en compilación

fn main() {
    const LOCAL: f64 = 3.141_592_653_589_793;  // válida en cualquier ámbito
    println!("{MAX_PUNTOS} {SEGUNDOS_POR_HORA} {LOCAL}");
}

Sus propiedades definitorias: no admite mut (es constante por definición), su valor debe ser un constant expression evaluable por el compilador, y puede declararse en cualquier ámbito, incluido dentro de una función. Gracias a las const fn, incluso puedes calcular constantes con funciones evaluadas en compilación.

static: un único lugar en memoria con vida ’static

Una static también es un valor global, pero con una diferencia semántica esencial: ocupa una dirección de memoria fija y única durante toda la ejecución del programa. Tiene, por tanto, vida 'static, la más larga posible.

static SALUDO: &str = "Hola, mundo";   // una sola instancia en memoria

fn main() {
    println!("{SALUDO}");
    println!("{:p}", SALUDO.as_ptr());  // tiene dirección: es referenciable
}

La distinción práctica: usa const para valores puros que quieres que se inserten en línea (la mayoría de los casos); usa static cuando necesitas que exista un objeto con una dirección estable —por ejemplo, un buffer grande que no quieres duplicar, o datos que expones por FFI a C.

⚠️
static mut es unsafe, y en la edición 2024 aún más restringido

Una static mut es estado global mutable: cualquier hilo puede escribirla, lo que reintroduce exactamente los data races que Rust erradica. Por eso acceder a ella exige un bloque unsafe. En la edición 2024, además, tomar referencias a una static mut está prohibido por defecto (lint static_mut_refs), empujándote hacia alternativas seguras como Atomic*, Mutex o OnceLock. Si crees necesitar static mut, casi siempre quieres una de esas.

Cuatro formas de nombrar un valor, cuatro contratos distintos

let, let mut, const y static parecen variaciones triviales de “guardar un valor”, pero cada una es un contrato semántico diferente, y dominarlas es dominar cómo Rust piensa el estado. let dice: este nombre apunta a un valor y no lo reasignaré. let mut dice: este lugar contiene estado que evolucionará, presta atención. const dice: esto no es una variable en absoluto, es un valor conocido en compilación que puedes tratar como una verdad fija insertada donde haga falta. static dice: esto es un objeto real, único, con dirección, que vive tanto como el programa. El shadowing corta transversalmente estos ejes: no es mutación disfrazada, es la afirmación de que el valor viejo ha cumplido su función y un valor nuevo —quizá de otro tipo— toma el relevo bajo el mismo nombre. La maestría no está en saber que estas herramientas existen, sino en que tu elección entre ellas comunique al siguiente lector —que muchas veces eres tú dentro de seis meses— qué clase de dato es este y cómo se comporta en el tiempo.

⚔️ Elige con intención
  1. Toma let x = "5"; y, por shadowing, conviértelo en un i32. Intenta lo mismo con mut y explica el error E0308.
  2. Ensombrece una variable dentro de un bloque { } y comprueba que fuera reaparece el valor original.
  3. Declara una const que combine dos constantes con aritmética y verifica que se evalúa en compilación (úsala como tamaño de un array [0; MI_CONST]).
  4. Declara una static e imprime su dirección con {:p}; razona por qué una const no tendría una dirección estable análoga.
  5. Investiga por qué static mut exige unsafe y qué ofrecen OnceLock o AtomicU32 como sustitutos seguros.