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

let y la inmutabilidad por defecto

Por qué Rust hace las variables inmutables salvo que pidas mut explícitamente, y qué gana ese default en seguridad, optimización y capacidad de razonar sobre el código.

⏱ 15 min

Casi todos los lenguajes te dan variables mutables por defecto y te obligan a pedir la inmutabilidad (const, final, val). Rust invierte el default: let crea un enlace inmutable, y la mutación es una decisión que declaras con mut. No es una manía estética; es una elección de diseño que se propaga hasta el borrow checker, el optimizador y la concurrencia.

🎯 Al terminar esta lección sabrás
  • Distinguir el enlace (let) del valor y de la mutabilidad del binding.
  • Entender qué garantías compra la inmutabilidad por defecto.
  • Ver cómo mut se coordina con el préstamo exclusivo &mut.
  • Separar inmutabilidad del binding de la mutabilidad interior.

El binding inmutable como default

let x = 5; no declara “una variable llamada x”. Declara un enlace (binding) del nombre x al valor 5, y por defecto ese enlace es de solo lectura. Reasignar es un error de compilación, no un aviso:

fn main() {
    let x = 5;
    x = 6; // error[E0384]: cannot assign twice to immutable variable `x`
    println!("{x}");
}

El compilador incluso te sugiere la corrección: añadir mut. La mutabilidad se pide de forma explícita y local, término a término:

fn main() {
    let mut contador = 0;
    contador += 1;      // ahora sí: el binding es mutable
    contador = 10;      // reasignación completa, también válida
    println!("{contador}");
}
ℹ️
mut califica al binding, no al tipo

let mut x: i32 no crea un tipo distinto de let x: i32. La mutabilidad es una propiedad del lugar (la variable), no del valor ni del tipo. Por eso puedes mover un valor desde un binding inmutable a uno mut (o al revés) sin conversión alguna: es el mismo i32, solo cambia quién tiene permiso para sobrescribir ese lugar.

Qué compra el default inmutable

La inmutabilidad por defecto no es una restricción gratuita: paga en tres monedas distintas.

Razonamiento local. Si x es inmutable, sabes que su valor no cambia entre su definición y su último uso, sin leer nada más. No hay que rastrear si una función intermedia lo modificó. El compilador convierte “seguramente no cambia” en “no puede cambiar”.

Optimización. Un valor que el compilador sabe inmutable puede mantenerse en un registro, propagarse como constante o reordenarse sin releer memoria. La ausencia de aliasing mutable (la regla de que solo puede existir un &mut a la vez) es precisamente lo que permite a rustc y a LLVM asumir que nadie mutará un dato a tus espaldas.

Concurrencia. Los datos inmutables compartidos entre hilos son seguros por construcción: sin escrituras no hay data races. Esta idea se formaliza más adelante en los traits Send y Sync, pero su raíz es este default.

flowchart TD
A[let x] --> B{Necesitas reasignar o mutar}
B -->|No| C[Deja el binding inmutable]
B -->|Si| D[Declara let mut x]
C --> E[Razonamiento local garantizado]
C --> F[Compartir entre hilos sin riesgo]
D --> G[Solo un prestamo mut a la vez]

mut y el préstamo exclusivo

La mutabilidad del binding es la puerta de entrada a la regla central de Rust: para modificar un dato a través de una referencia necesitas un préstamo mutable &mut, y solo puede existir uno simultáneamente, sin ningún & compartido activo a la vez.

fn main() {
    let mut v = vec![1, 2, 3];
    let r = &mut v;      // préstamo exclusivo
    r.push(4);
    // mientras r viva, no puedes tomar otro & ni &mut de v
    println!("{v:?}");   // aquí r ya no se usa: v vuelve a estar libre
}

Sin mut en el binding, &mut v ni siquiera compila. Así, la mutabilidad viaja explícita desde la declaración (let mut) hasta cada préstamo (&mut). El compilador puede seguir esa cadena y demostrar que nunca hay dos escritores, o un escritor y un lector, sobre el mismo dato al mismo tiempo. Este es el “aliasing XOR mutability” que estudiarás a fondo en el nivel de ownership.

Inmutabilidad del binding no es congelar el valor

Cuidado con una confusión común: let inmoviliza el enlace, no necesariamente todo lo alcanzable desde él. Existe la mutabilidad interior: tipos como Cell<T> y RefCell<T> permiten mutar su contenido a través de una referencia compartida &, moviendo la comprobación de préstamo de compilación a ejecución.

use std::cell::Cell;

fn main() {
    let celda = Cell::new(0);   // ¡binding inmutable!
    celda.set(42);              // y aun así mutamos el interior
    println!("{}", celda.get());
}

Esto no rompe el modelo: es una vía controlada y explícita, marcada por el tipo, para casos donde la disciplina estática de &mut es demasiado rígida. La regla mental correcta no es “inmutable = nada cambia”, sino “let = este nombre no se reasigna; para mutar el valor necesito o bien mut y &mut, o bien un tipo que ofrezca mutabilidad interior”.

El default correcto es el que hace lo seguro sin esfuerzo

Poner la inmutabilidad como default es una tesis sobre economía del error humano. En un lenguaje mutable-por-defecto, cada variable es un canal potencial de efectos a distancia, y evitar bugs exige disciplina activa: recordar marcar como constante lo que no debe cambiar. Rust invierte la carga: lo seguro es lo que ocurre si no haces nada, y la mutación —el ingrediente peligroso— es lo que debes escribir a propósito. El resultado es que mut se vuelve una señal: cuando lo lees, sabes que ahí hay estado que evoluciona y que merece tu atención. En una base de código madura, la mayoría de los bindings son inmutables, y los pocos mut marcan con precisión dónde vive la complejidad. Ese es el verdadero regalo: no que no puedas mutar, sino que la mutación deje de ser invisible.

⚔️ Interioriza el default
  1. Declara let x = 10; e intenta reasignarlo. Lee el error E0384 completo y su sugerencia.
  2. Convierte x en let mut y reasígnalo dos veces; confirma que compila.
  3. Toma let mut v = vec![1,2,3];, crea let r = &mut v;, y trata de imprimir v mientras usas r después. Observa el conflicto de préstamos.
  4. Usa un Cell<i32> con binding inmutable y muta su valor con set. Explica por qué no contradice a let.
  5. Recorre tu último programa mental o real y cuenta cuántos bindings necesitan de verdad mut.