wandres.dev
DEREF, DROP Y OPERADORES · traits que dan superpoderes

Deref y la coerción: por qué una ref a String vale donde se pide ref a str

`Deref` sobrecarga el operador `*`, pero su verdadero poder es la coerción por deref: el compilador convierte en silencio un `&String` en `&str`, un `&Vec<T>` en `&[T]`, un `&Box<T>` en `&T`. Un puntero inteligente que implementa `Deref` se comporta como el valor que envuelve, y por eso `String` alcanza todos los métodos de `str` sin redefinir ninguno.

⏱ 19 min

Deref sobrecarga el operador *, pero reducirlo a eso es perderse lo importante. Su verdadera consecuencia es la coerción por deref: el compilador convierte en silencio un &String en &str, un &Vec<T> en &[T], un &Box<T> en &T, allí donde hace falta. Un puntero inteligente que implementa Deref deja de ser una caja opaca y se comporta como el valor que envuelve. Esa es la razón profunda de que String disponga de todos los métodos de str sin redefinir ni uno: no los hereda, los alcanza por deref.

🎯 Al terminar esta lección sabrás
  • Sobrecargar el operador * implementando Deref con su tipo Target.
  • Entender la coerción por deref en los sitios de llamada y en la resolución de métodos.
  • Explicar por qué &String se convierte en &str y &Vec<T> en &[T].
  • Reconocer Deref como la interfaz del puntero inteligente y su anti-patrón.

Deref: el operador asterisco a tu medida

El trait tiene un tipo asociado Target —el valor apuntado— y un método que devuelve una referencia a él:

pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

Cuando escribes *x sobre algo que no es una referencia ni un puntero crudo, el compilador lo expande a *(x.deref()). Construyamos nuestra propia caja para verlo:

use std::ops::Deref;

struct MiCaja<T>(T);

impl<T> MiCaja<T> {
    fn new(x: T) -> MiCaja<T> { MiCaja(x) }
}

impl<T> Deref for MiCaja<T> {
    type Target = T;
    fn deref(&self) -> &T { &self.0 }
}

fn main() {
    let c = MiCaja::new(5);
    assert_eq!(5, *c);   // *c es *(c.deref()), es decir *(&c.0)
}

La coerción por deref

Aquí empieza lo interesante. Cuando pasas un &T donde se espera un &U, y T implementa Deref<Target = U>, el compilador inserta la conversión por ti. Y lo hace de forma transitiva, encadenando tantos deref como haga falta:

fn hola(nombre: &str) {
    println!("Hola, {nombre}");
}

fn main() {
    let m = MiCaja::new(String::from("Ada"));
    hola(&m);   // &MiCaja<String> -> &String -> &str, dos coerciones seguidas
}
flowchart LR
A[ref a MiCaja de String] -->|Deref Target String| B[ref a String]
B -->|Deref Target str| C[ref a str]
C --> D[Encaja en el parametro ref a str]
style A fill:#89b4fa,color:#11111b
style C fill:#a6e3a1,color:#11111b
style D fill:#cba6f7,color:#11111b

Sin la coerción tendrías que escribir algo como hola(&(*m)[..]). Con ella, &m basta. Y es coste cero: todo se resuelve en compilación, sin indirección extra en tiempo de ejecución.

ℹ️
Las tres coerciones, y la que no existe

El compilador aplica tres conversiones por deref: &T a &U cuando T: Deref<Target = U>; &mut T a &mut U cuando T: DerefMut<Target = U>; y &mut T a &U cuando T: Deref<Target = U> (degradar de mutable a compartido siempre es seguro). La cuarta dirección —de &T a &mut Uno existe: no puedes ganar mutabilidad por el camino, porque eso rompería el aliasing. La mutabilidad solo se pierde en las coerciones, nunca se gana.

Resolución de métodos y la cadena de deref

La coerción no solo actúa en los argumentos: gobierna también la resolución de métodos. Cuando escribes x.metodo(), el compilador prueba x, luego &x, luego &mut x; si nada encaja, aplica un deref y repite, y así hasta el final de la cadena. Por eso un String responde a los métodos de str, y un Box<T> a los métodos de T:

fn main() {
    let s = String::from("hola mundo");
    // len() y split_whitespace() son metodos de str, no de String:
    let n = s.len();                             // String -> str por deref
    let palabras = s.split_whitespace().count();
    assert_eq!((n, palabras), (10, 2));
}

String no reimplementa len; el compilador desciende de String a str por su Deref y encuentra el método allí. La caja y el contenido comparten interfaz sin una sola línea de reenvío manual.

Deref es la interfaz del puntero inteligente

Deref existe para una cosa: hacer que un puntero inteligente —un tipo que envuelve y posee un valor con alguna lógica añadida— se use como si fuera el valor. Box<T>, Rc<T>, String sobre str, Vec<T> sobre [T]: todos lo implementan para volverse transparentes.

⚠️
No uses Deref para emular herencia

La tentación, sobre todo viniendo de la orientación a objetos, es implementar Deref de un tipo Empleado hacia un tipo Persona para “heredar” sus métodos. Es un anti-patrón reconocido: Deref significa este tipo es un puntero a aquel, no este tipo es un subtipo de aquel. Abusar de él enturbia la resolución de métodos, sorprende a quien lee el código y se rompe cuando dos capas exponen métodos con el mismo nombre. Implementa Deref solo cuando tu tipo sea genuinamente un puntero a su Target; para reutilizar comportamiento, usa composición y traits.

La coerción por deref es la abstracción de la indirección hecha invisible y gratis

Detente en lo que ocurre de verdad cuando hola(&m) compila. El Target de un tipo es, matemáticamente, una función que va del tipo puntero al tipo apuntado: MiCaja<String> apunta a String, y String apunta a str. La coerción por deref es la composición de esas funciones, calculada por el compilador hasta que el tipo resultante encaja con lo que se pide, y luego borrada: en el binario no queda ni rastro de la cadena, solo el acceso final. Esto es lo que permite que en Rust envolver un valor en capas de indirección —una Box dentro de un Rc, un String que por dentro es un Vec<u8> que es un [u8]— no cueste ni sintaxis ni rendimiento en el punto de uso: la envoltura desaparece. Y, a diferencia de las conversiones implícitas de C++, que son una fuente clásica de sorpresas porque se disparan casi en cualquier sitio, la de Rust está domada por tres restricciones: solo actúa sobre referencias, solo sigue un Target declarado explícitamente por un impl Deref, y nunca inventa mutabilidad que no tuvieras. El resultado es una de esas raras conquistas de diseño en las que la ergonomía y el rigor no se pelean: escribes &m como si m fuera ya el valor, el compilador demuestra que la cadena de punteros lo justifica, y no pagas nada por la comodidad. Deref no es una utilidad para punteros; es la afirmación de que en Rust la indirección puede ser transparente sin ser mágica.

📝
Lo esencial de Deref y la coerción

Deref sobrecarga *: *x es *(x.deref()), con un tipo asociado Target. Su fruto mayor es la coerción por deref, que convierte &T en &U cuando T: Deref<Target = U>, de forma transitiva y en compilación. Por eso &String vale como &str y &Vec<T> como &[T], y por eso la resolución de métodos alcanza los del Target: String usa los métodos de str sin redefinirlos. Implementa Deref solo para punteros inteligentes de verdad, nunca para emular herencia.

⚔️ Sigue la cadena de punteros
  1. Define struct MiCaja<T>(T) con su Deref y comprueba que *MiCaja::new(10) == 10. Explica en qué se expande el *.
  2. Escribe fn saluda(n: &str) y llámala pasando &MiCaja::new(String::from("Ada")). Nombra las dos coerciones que ocurren, en orden.
  3. Sobre un String, invoca tres métodos que en realidad pertenezcan a str y explica cómo los alcanza la resolución de métodos.
  4. Intenta pasar un &MiCaja<i32> a una función que pide &mut i32 y lee el error. Relaciónalo con la coerción que “no existe”.
  5. Explica por qué implementar Deref de Empleado a Persona es un anti-patrón, y qué usarías en su lugar.