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.
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.
- Sobrecargar el operador
*implementandoDerefcon su tipoTarget. - Entender la coerción por deref en los sitios de llamada y en la resolución de métodos.
- Explicar por qué
&Stringse convierte en&stry&Vec<T>en&[T]. - Reconocer
Derefcomo 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.
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 U— no 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.
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.
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.
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.
- Define
struct MiCaja<T>(T)con suDerefy comprueba que*MiCaja::new(10) == 10. Explica en qué se expande el*. - Escribe
fn saluda(n: &str)y llámala pasando&MiCaja::new(String::from("Ada")). Nombra las dos coerciones que ocurren, en orden. - Sobre un
String, invoca tres métodos que en realidad pertenezcan astry explica cómo los alcanza la resolución de métodos. - Intenta pasar un
&MiCaja<i32>a una función que pide&mut i32y lee el error. Relaciónalo con la coerción que “no existe”. - Explica por qué implementar
DerefdeEmpleadoaPersonaes un anti-patrón, y qué usarías en su lugar.