DerefMut y la transparencia de Box, Rc y compañía
`Deref` hizo legible el puntero; `DerefMut` lo hace escribible. Juntos vuelven transparente a `Box<T>`: se comporta como el valor que envuelve para leer, llamar métodos y asignar. Pero `Rc` y `Arc` implementan `Deref` y no `DerefMut`, y esa ausencia es deliberada: la propiedad compartida prohíbe el acceso exclusivo. Que un puntero deje mutar a su través es una afirmación a nivel de tipo.
Deref hizo que un puntero se pudiera leer a su través; DerefMut hace que se pueda escribir. Juntos convierten a Box<T> en algo transparente: se comporta como un T para leer, para llamar a sus métodos y para asignarle un valor nuevo. Pero no todos los punteros inteligentes implementan DerefMut, y la ausencia es tan significativa como la presencia: Rc<T> y Arc<T> se quedan solo en Deref, porque la propiedad compartida hace imposible entregar un &mut seguro. Que un puntero te deje mutar a su través es una afirmación a nivel de tipo, decidida en compilación, no una propiedad de tiempo de ejecución.
- Implementar
DerefMut(con supertraitDeref) para el deref mutable. - Entender cómo
Box<T>se vuelve transparente para leer y para escribir. - Explicar por qué
RcyArcimplementanDerefpero noDerefMut. - Relacionar los guardas (
RefMut,MutexGuard) conDerefMut.
DerefMut: escribir a través del puntero
DerefMut tiene a Deref como supertrait: no puedes implementar el mutable sin el compartido. Su método devuelve una referencia mutable al Target:
pub trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
Con él, *x = valor se expande a *x.deref_mut() = valor, y &mut *x te da acceso mutable al interior. Que DerefMut exija Deref no es burocracia: garantiza que el Target mutable y el compartido son el mismo tipo, de modo que leer y escribir apuntan al mismo lugar.
Box es transparente para leer y para escribir
Box<T> implementa ambos traits, así que desaparece en el punto de uso: puedes llamar métodos &self y &mut self del interior, indexar y reasignar el valor entero.
fn main() {
let mut b = Box::new(vec![1, 2, 3]);
b.push(4); // metodo &mut self de Vec, alcanzado por DerefMut
b[0] = 10; // asignacion a traves del Box (Index + DerefMut)
*b = vec![9]; // reasigna el Vec entero, via deref_mut
println!("{b:?}"); // [9]
}
En ningún momento escribes .deref() ni .deref_mut(): el compilador los inserta al resolver métodos y al desazucarar el *. La caja es, a efectos de uso, el Vec que contiene.
Hay algo que Box<T> puede hacer y que Deref no sabe expresar: mover el valor fuera de la caja. Un let v = *b; sobre un Box<Vec<i32>> no copia ni presta el Vec; lo saca del heap y te lo entrega por valor, consumiendo la caja. Esta operación —a veces llamada DerefMove— está cableada en el compilador solo para Box, porque Box es dueño único y puede ceder su contenido. No existe para Rc ni Arc, que al ser dueños compartidos no pueden dejar que nadie se lleve el valor, ni es representable por los traits Deref o DerefMut, cuyos métodos solo devuelven referencias. Es un recordatorio de que la transparencia de Box va un paso más allá de la de cualquier otro puntero.
Rc y Arc: Deref sí, DerefMut no
Aquí la ausencia habla. Rc<T> (contador de referencias de un solo hilo) y Arc<T> (su versión atómica, entre hilos) permiten varios dueños del mismo valor. Si implementaran DerefMut, podrías obtener un &mut T mientras otro Rc observa el mismo T: dos accesos, uno de ellos mutable, al mismo dato —precisamente el aliasing que las reglas de préstamo prohíben—. Por eso solo implementan Deref:
use std::rc::Rc;
fn main() {
let compartido = Rc::new(5);
let _otro = Rc::clone(&compartido); // dos duenos: hay aliasing
println!("{}", *compartido); // leer a traves: OK, via Deref
// *compartido = 6; // ERROR: Rc no implementa DerefMut
}
Para mutar un valor con varios dueños necesitas mutabilidad interior: trasladar la comprobación del aliasing a tiempo de ejecución con RefCell (un hilo) o Mutex (varios hilos), envueltos dentro del Rc o del Arc.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let compartido = Rc::new(RefCell::new(5));
*compartido.borrow_mut() += 1; // RefMut SI implementa DerefMut
assert_eq!(*compartido.borrow(), 6);
}
Los guardas: DerefMut con disciplina
El patrón de borrow_mut revela quién hace el trabajo. RefCell no implementa DerefMut; te entrega un guarda, un valor temporal (Ref o RefMut) que sí lo implementa y que, al morir, devuelve el préstamo. Lo mismo hacen los cerrojos: Mutex::lock da un MutexGuard que implementa DerefMut, y RwLock reparte un guarda de lectura (solo Deref) y uno de escritura (DerefMut).
flowchart TB P[Puntero inteligente] --> Q[Garantiza acceso exclusivo] Q -->|si Box y guardas| M[Implementa Deref y DerefMut] Q -->|no Rc y Arc| R[Implementa solo Deref] M --> W[Puedes leer y escribir a traves] R --> L[Solo lectura a traves] L --> I[Para mutar usa RefCell o Mutex dentro] style M fill:#a6e3a1,color:#11111b style R fill:#f38ba8,color:#11111b style W fill:#89b4fa,color:#11111b style I fill:#cba6f7,color:#11111b
Así, *guard += 1 funciona por el DerefMut del guarda, y el Drop del guarda suelta el cerrojo o cierra el préstamo. La transparencia mutable y la liberación determinista trabajan juntas.
Lo profundo aquí es que la misma operación —añadir impl DerefMut— es correcta para Box y sería catastrófica para Rc, y el lenguaje codifica esa diferencia donde debe: en el sistema de tipos. Box<T> es dueño único de su T; darte un &mut T a su través respeta la regla de oro de que un préstamo mutable es exclusivo, porque no hay ningún otro camino hacia ese valor. Rc<T> es dueño compartido; si implementara DerefMut, dos Rc clonados podrían fabricar dos &mut al mismo dato, y todo el edificio de la seguridad de memoria —que descansa en “mientras haya un &mut, no hay ningún otro acceso”— se derrumbaría en silencio. Por eso la biblioteca estándar, sencillamente, no implementa DerefMut para Rc, y el compilador, en consecuencia, rechaza *rc = x en compilación con la misma naturalidad con la que aceptaría *caja = x. No es una comprobación en tiempo de ejecución ni una convención que se pueda romper: es la ausencia de un impl, y esa ausencia es la demostración de que Rc no puede prometer exclusividad. La mutabilidad interior —RefCell, Mutex— no viola esto, sino que lo respeta cambiando el momento de la prueba: como no puede demostrarse la exclusividad estáticamente, la verifican en ejecución y penalizan la violación con un panic o un bloqueo. La lección trasciende los punteros: en Rust, “puedo mutar esto a su través” no es una capacidad que se conceda por comodidad, sino una afirmación que un tipo solo hace cuando puede sostenerla, y la presencia o ausencia de DerefMut es la firma de esa promesa.
DerefMut (supertrait de Deref) da el deref mutable: *x = v es *x.deref_mut() = v. Con Deref y DerefMut, Box<T> se vuelve transparente para leer, llamar métodos &mut self y reasignar. Rc y Arc implementan solo Deref, no DerefMut, porque la propiedad compartida prohíbe entregar un &mut exclusivo; para mutar se usa mutabilidad interior (Rc<RefCell<T>>, Arc<Mutex<T>>), cuyos guardas RefMut y MutexGuard sí implementan DerefMut y sueltan el recurso en su Drop.
- Crea un
Box<Vec<i32>>, mútalo conpush, indexa para asignarb[0] = 10y reasigna elVecentero con*b = .... Nombra qué trait interviene en cada caso. - Crea un
Rc<i32>, clónalo e intenta*rc = 6. Lee el error y relaciónalo con el aliasing de la propiedad compartida. - Envuelve un valor en
Rc<RefCell<T>>y mútalo conborrow_mut. Explica qué guarda te devuelve y qué trait implementa. - Repite con
Arc<Mutex<T>>en un contexto de varios hilos y observa queMutexGuardimplementaDerefMuty suelta el cerrojo al caer. - Explica con tus palabras por qué implementar
DerefMutparaRcrompería la seguridad de memoria, y qué significa que la biblioteca simplemente no lo implemente.