El problema: mutar a través de una referencia compartida
El borrow checker impone una ley de hierro: compartir O mutar, nunca a la vez. Un `&T` deja leer pero no escribir; un `&mut T` deja escribir pero exige ser la única referencia viva. Y sin embargo hay código legítimo que necesita mutar a través de un `&` compartido: el recuento de un `Rc` que sube al clonarlo, una caché que se rellena en el primer acceso, un contador escondido en un método `&self`. La mutabilidad interior es la vía de escape controlada a esa ley.
La regla que gobierna todo el modelo de préstamos de Rust cabe en cinco palabras: compartir o mutar, nunca ambas. A través de un &T puedes leer cuanto quieras, pero no escribir; a través de un &mut T puedes escribir, pero el compilador exige que sea la única referencia viva a ese dato. Esa disyunción —aliasing XOR mutación— es lo que borra de raíz las carreras de datos y los punteros colgantes. Pero es una regla, y las reglas útiles tienen excepciones legítimas. A veces necesitas de verdad mutar un dato mientras varias referencias compartidas lo observan: el recuento de un Rc que se incrementa al clonarlo con &self, una caché perezosa que se rellena en el primer acceso, un contador de visitas dentro de un método &self. La mutabilidad interior es el mecanismo con el que Rust concede esa excepción sin renunciar a la seguridad.
- Enunciar la regla compartir-o-mutar y por qué prohíbe escribir a través de
&T. - Reconocer los casos legítimos que exigen mutar tras una referencia compartida.
- Distinguir la mutabilidad heredada de la mutabilidad interior.
- Situar
Cell,RefCellyUnsafeCellcomo las tres puertas a ese patrón.
La ley de hierro del borrow checker
En Rust la mutabilidad no es una propiedad del dato, sino del camino de acceso por el que lo alcanzas. Un valor es mutable solo si llegas a él a través de un dueño con binding mut o de una referencia &mut. Esto se llama mutabilidad heredada: la capacidad de escribir se hereda del camino, no reside en la variable. Y de ahí sale la prohibición tajante: si solo tienes un &T, no puedes mutar, y el compilador lo rechaza sin contemplaciones.
struct Contador {
visitas: u32,
}
impl Contador {
fn registrar(&self) {
// self.visitas += 1; // ERROR: cannot assign, `self` es un `&Contador`
}
}
&self es azúcar de self: &Contador: una referencia compartida. Escribir a través de ella violaría la regla, porque nada garantiza que no exista otra referencia leyendo ese mismo campo en el mismo instante. El compilador no distingue “aquí no hay nadie más”: para poder demostrarlo estáticamente, prohíbe el caso entero.
Cuándo la regla estorba
El problema es que hay diseños perfectamente correctos que la regla veta. El más revelador vive en la propia biblioteca estándar: Rc<T>, el puntero de propiedad compartida, lleva dentro un recuento de referencias que debe subir al clonar y bajar al soltar. Pero clonar un Rc toma &self —tiene que ser así: clonar es compartir, no consumir—, y aun así muta ese recuento:
use std::rc::Rc;
let a = Rc::new(String::from("dato"));
let b = Rc::clone(&a); // &a es compartido...
println!("{}", Rc::strong_count(&a)); // ...y el recuento ya vale 2
drop(b);
println!("{}", Rc::strong_count(&a)); // 1 otra vez
Sin mutar tras un &, Rc no podría existir. Y no está solo: una caché perezosa que se calcula en el primer plan(&self), un objeto mock que registra cuántas veces lo llamaron en un test, un nodo de grafo con aristas que apuntan a vecinos compartidos, el patrón observador que notifica a suscriptores… todos necesitan escribir a través de referencias compartidas.
struct Consulta {
sql: String,
plan: Option<String>, // se calcularia una vez y se reutilizaria
}
impl Consulta {
fn plan(&self) -> &str {
// if self.plan.is_none() {
// self.plan = Some(compilar(&self.sql)); // ERROR: `self` es `&Consulta`
// }
self.plan.as_deref().unwrap_or("")
}
}
La regla, tomada al pie de la letra, declararía imposible esta caché tan corriente: plan toma &self porque consultar no debería exigir posesión exclusiva, y sin embargo querría rellenar el campo la primera vez. La mutabilidad interior es justo lo que desatasca este patrón.
Dos mundos: heredada frente a interior
Mutabilidad heredada
La norma. Mutas si posees el valor o lo alcanzas por un &mut. El compilador la verifica estáticamente: coste cero, ninguna sorpresa en ejecución.
Mutabilidad interior
La excepción. Un tipo envoltorio te deja mutar su contenido a través de un & compartido y garantiza la seguridad por otra vía: sin dar referencias (Cell) o con un chequeo en ejecución (RefCell).
La mutabilidad interior no rompe la regla: la sustituye por una garantía equivalente aplicada de otra manera. En vez de que el compilador demuestre en tiempo de compilación que no hay aliasing peligroso, es el propio tipo el que se encarga de que la mutación sea segura —moviendo valores enteros en lugar de prestar referencias, o llevando un registro de préstamos que consulta en cada acceso.
Las tres puertas
Rust ofrece tres tipos para entrar en este territorio, de mayor a menor abstracción:
Cell<T>— mueve valores dentro y fuera congetyset, sin entregar jamás una referencia al interior. Seguro por ausencia de referencias. Coste cero.RefCell<T>— presta referencias conborrowyborrow_mut, pero comprueba la regla compartir-o-mutar en tiempo de ejecución y hacepanicsi la violas.UnsafeCell<T>— la primitiva cruda del lenguaje sobre la que se construyen las dos anteriores; la única puerta que el compilador acepta para mutar tras un&, y por dentro requiereunsafe.
flowchart TB regla[Compartir o mutar nunca ambas] --> choque[Necesito mutar tras una referencia compartida] choque --> cell[Cell mueve valores sin dar referencias] choque --> refcell[RefCell presta y comprueba en ejecucion] choque --> unsafe[UnsafeCell la primitiva cruda] cell --> seguro[API segura por fuera] refcell --> seguro unsafe --> nucleo[Nucleo unsafe por dentro] nucleo --> seguro style regla fill:#f38ba8,color:#11111b style seguro fill:#a6e3a1,color:#11111b style unsafe fill:#89b4fa,color:#11111b
Elegir la puerta correcta es cosa de preguntarse qué necesitas hacer con el interior, en este orden:
- Si el dato es
Copyy pequeño —un contador, una bandera, un índice— y te basta leerlo y sobrescribirlo entero:Cell<T>. Es la más barata y no puede hacerpanic. - Si necesitas prestar el interior —mutar en el sitio un
Vec, invocar métodos&mutsobre él, leer sin copiar—:RefCell<T>, asumiendo el chequeo en ejecución y el riesgo depanic. - Si escribes tú una abstracción de mutabilidad interior nueva y ninguna de las anteriores encaja:
UnsafeCell<T>, con la responsabilidad de demostrar la seguridad a mano.
Y una regla transversal: si el estado se comparte entre hilos, ninguna de las tres sirve directamente —son todas !Sync—; ahí entran Mutex, RwLock y los tipos atómicos, que cierran el patrón más adelante.
No confundas mutabilidad interior con las variables globales mutables de C. Aquí no hay estado ambiental oculto: la mutación sigue localizada dentro de un valor concreto que alguien posee, y las garantías de seguridad —no hay dos escrituras que se pisen— se mantienen intactas. Lo único que cambia es quién y cuándo las comprueba: el tipo envoltorio, en vez del borrow checker; en ejecución, en vez de en compilación.
Detente en qué demuestra de verdad el borrow checker. El invariante que Rust necesita para la seguridad de memoria es más estrecho de lo que la regla enuncia: exige que ninguna escritura ocurra a la vez que otro acceso al mismo dato, es decir, que no haya un acceso mutable simultáneo a otro cualquiera. “Compartir XOR mutar” es una aproximación conservadora y estáticamente verificable de ese invariante: una condición suficiente —si la cumples, estás a salvo— pero no necesaria —puedes estar a salvo sin cumplirla al pie de la letra—. El precio de esa conservadurez es que el compilador rechaza programas correctos: sabe que Rc::clone es seguro, pero no puede probarlo con las herramientas del análisis estático, así que, de no existir una escapatoria, lo prohibiría. La mutabilidad interior es precisamente esa escapatoria disciplinada: conserva el invariante verdadero —nunca dos accesos en conflicto— pero lo hace cumplir por un camino distinto al de la regla sintáctica. Cell lo garantiza aboliendo las referencias al interior, de modo que no hay aliasing posible que violar; RefCell lo garantiza contando los préstamos en ejecución y abortando antes de permitir un conflicto; Mutex lo garantiza serializando el acceso con un cerrojo. En los tres casos la seguridad no se debilita ni un ápice: solo se traslada el momento y el mecanismo de la prueba, del compilador al tipo, de la compilación a la ejecución. Comprender esto disuelve la sensación de estar “haciendo trampa”: no engañas al sistema de tipos, operas en la franja legítima que separa lo demostrable estáticamente de lo verdaderamente seguro. Ahí, en esa franja, vive toda la mutabilidad interior, y con ella Rc, los grafos, las cachés y buena parte de la infraestructura sobre la que se apoya el resto del lenguaje.
La mutabilidad en Rust es heredada: escribes solo por un camino de dueño o &mut, nunca a través de un &T compartido. Pero hay diseños legítimos que necesitan justo eso —el recuento de Rc, cachés, observadores, grafos—, y la regla compartir-o-mutar, que es suficiente pero no necesaria para la seguridad, los prohibiría. La mutabilidad interior es la excepción controlada: tres tipos —Cell, RefCell y UnsafeCell— que permiten mutar tras un & conservando el invariante verdadero por una vía distinta a la del borrow checker.
- Escribe un
struct Contador { visitas: u32 }con un métodoregistrar(&self)que intenteself.visitas += 1. Lee el error del compilador y explica por qué&selflo impide. - Con
Rc::newyRc::clone, imprimeRc::strong_countantes y después de clonar. Argumenta por qué esa cuenta tiene que mutar a través de un&. - Define con tus palabras la diferencia entre mutabilidad heredada y mutabilidad interior, sin usar la palabra “trampa”.
- Enumera tres casos reales (fuera de
Rc) que exijan mutar tras una referencia compartida y di qué tienen en común. - Explica por qué “compartir XOR mutar” es una condición suficiente pero no necesaria para la seguridad de memoria, y qué papel juega la mutabilidad interior en esa distinción.