Introducción
Bienvenido a Tratar Punteros Inteligentes como Referencias Normales con Deref. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.
En esta práctica, exploraremos cómo implementar el trato Deref permite que los punteros inteligentes se traten como referencias normales y cómo la característica de coerción de deref de Rust permite trabajar con referencias o punteros inteligentes.
Tratar Punteros Inteligentes como Referencias Normales con Deref
Implementar el trato Deref te permite personalizar el comportamiento del operador de dereferencia * (no confundir con el operador de multiplicación o glob). Al implementar Deref de manera que un puntero inteligente se pueda tratar como una referencia normal, puedes escribir código que opere sobre referencias y usar ese código con punteros inteligentes también.
Veamos primero cómo funciona el operador de dereferencia con referencias normales. Luego intentaremos definir un tipo personalizado que se comporte como Box<T> y veremos por qué el operador de dereferencia no funciona como una referencia en nuestro nuevo tipo definido. Exploraremos cómo implementar el trato Deref hace posible que los punteros inteligentes funcionen de manera similar a las referencias. Luego veremos la característica de coerción de deref de Rust y cómo nos permite trabajar con referencias o punteros inteligentes.
Nota: Hay una gran diferencia entre el tipo
MyBox<T>que construiremos y el realBox<T>: nuestra versión no almacenará sus datos en el montón. Este ejemplo se centra enDeref, por lo que donde se almacenan realmente los datos es menos importante que el comportamiento parecido a un puntero.
Siguiendo el Puntero Hasta el Valor
Una referencia normal es un tipo de puntero, y una forma de pensar en un puntero es como una flecha hacia un valor almacenado en otro lugar. En la Lista 15-6, creamos una referencia a un valor i32 y luego usamos el operador de dereferencia para seguir la referencia hasta el valor.
Nombre de archivo: src/main.rs
fn main() {
1 let x = 5;
2 let y = &x;
3 assert_eq!(5, x);
4 assert_eq!(5, *y);
}
Lista 15-6: Usando el operador de dereferencia para seguir una referencia a un valor i32
La variable x contiene un valor i32 de 5 [1]. Establecemos y igual a una referencia a x [2]. Podemos afirmar que x es igual a 5 [3]. Sin embargo, si queremos hacer una afirmación sobre el valor en y, tenemos que usar *y para seguir la referencia hasta el valor a que apunta (de ahí el nombre dereferencia) para que el compilador pueda comparar el valor real [4]. Una vez que desreferenciamos y, tenemos acceso al valor entero al que y apunta que podemos comparar con 5.
Si intentáramos escribir assert_eq!(5, y); en su lugar, obtendríamos este error de compilación:
error[E0277]: no se puede comparar `{entero}` con `&{entero}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementación para `{entero} ==
&{entero}`
|
= ayuda: la característica `PartialEq<&{entero}>` no está implementada
para `{entero}`
No se permite comparar un número y una referencia a un número porque son tipos diferentes. Debemos usar el operador de dereferencia para seguir la referencia hasta el valor a que apunta.
Usando Box<T> Como una Referencia
Podemos reescribir el código de la Lista 15-6 para usar un Box<T> en lugar de una referencia; el operador de dereferencia usado en el Box<T> de la Lista 15-7 funciona de la misma manera que el operador de dereferencia usado en la referencia de la Lista 15-6.
Nombre de archivo: src/main.rs
fn main() {
let x = 5;
1 let y = Box::new(x);
assert_eq!(5, x);
2 assert_eq!(5, *y);
}
Lista 15-7: Usando el operador de dereferencia en un Box<i32>
La principal diferencia entre la Lista 15-7 y la Lista 15-6 es que aquí establecemos y como una instancia de un box que apunta a una copia del valor de x en lugar de una referencia que apunta al valor de x [1]. En la última afirmación [2], podemos usar el operador de dereferencia para seguir el puntero del box de la misma manera que lo hicimos cuando y era una referencia. A continuación, exploraremos lo que es especial de Box<T> que nos permite usar el operador de dereferencia definiendo nuestro propio tipo de box.
Definiendo Nuestro Propio Puntero Inteligente
Vamos a construir un puntero inteligente similar al tipo Box<T> proporcionado por la biblioteca estándar para experimentar cómo los punteros inteligentes se comportan de manera diferente a las referencias por defecto. Luego veremos cómo agregar la capacidad de usar el operador de dereferencia.
El tipo Box<T> se define en última instancia como una struct tupla con un elemento, por lo que la Lista 15-8 define un tipo MyBox<T> de la misma manera. También definiremos una función new para coincidir con la función new definida en Box<T>.
Nombre de archivo: src/main.rs
1 struct MyBox<T>(T);
impl<T> MyBox<T> {
2 fn new(x: T) -> MyBox<T> {
3 MyBox(x)
}
}
Lista 15-8: Definiendo un tipo MyBox<T>
Definimos una struct llamada MyBox y declaramos un parámetro genérico T [1] porque queremos que nuestro tipo pueda contener valores de cualquier tipo. El tipo MyBox es una struct tupla con un elemento de tipo T. La función MyBox::new toma un parámetro de tipo T [2] y devuelve una instancia de MyBox que contiene el valor pasado [3].
Intentemos agregar la función main de la Lista 15-7 a la Lista 15-8 y cambiarla para que use el tipo MyBox<T> que hemos definido en lugar de Box<T>. El código de la Lista 15-9 no se compilará porque Rust no sabe cómo desreferenciar MyBox.
Nombre de archivo: src/main.rs
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Lista 15-9: Intentando usar MyBox<T> de la misma manera que usamos referencias y Box<T>
Aquí está el error de compilación resultante:
error[E0614]: el tipo `MyBox<{entero}>` no se puede desreferenciar
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
Nuestro tipo MyBox<T> no se puede desreferenciar porque no hemos implementado esa capacidad en nuestro tipo. Para habilitar la desreferencia con el operador *, implementamos el trato Deref.
Implementando el Trato Deref
Como se discutió en "Implementando un Trato en un Tipo", para implementar un trato necesitamos proporcionar implementaciones para los métodos requeridos por el trato. El trato Deref, proporcionado por la biblioteca estándar, nos exige implementar un método llamado deref que presta self y devuelve una referencia a los datos internos. La Lista 15-10 contiene una implementación de Deref para agregar a la definición de MyBox<T>.
Nombre de archivo: src/main.rs
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
1 type Target = T;
fn deref(&self) -> &Self::Target {
2 &self.0
}
}
Lista 15-10: Implementando Deref en MyBox<T>
La sintaxis type Target = T; [1] define un tipo asociado para que el trato Deref lo use. Los tipos asociados son una forma ligeramente diferente de declarar un parámetro genérico, pero por ahora no tienes que preocuparte por ellos; los cubriremos con más detalle en el Capítulo 19.
Rellenamos el cuerpo del método deref con &self.0 para que deref devuelva una referencia al valor que queremos acceder con el operador * [2]; recuerda de "Usando Structs Tupla Sin Campos con Nombres para Crear Diferentes Tipos" que .0 accede al primer valor en una struct tupla. La función main de la Lista 15-9 que llama a * en el valor MyBox<T> ahora se compila y las afirmaciones se pasan correctamente.
Sin el trato Deref, el compilador solo puede desreferenciar referencias &. El método deref le da al compilador la capacidad de tomar un valor de cualquier tipo que implemente Deref y llamar al método deref para obtener una referencia & que sabe cómo desreferenciar.
Cuando escribimos *y en la Lista 15-9, detrás de escena Rust realmente ejecutó este código:
*(y.deref())
Rust sustituye el operador * con una llamada al método deref y luego una desreferencia normal para que no tengamos que pensar en si necesitamos llamar al método deref o no. Esta característica de Rust nos permite escribir código que funciona de manera idéntica ya sea que tengamos una referencia normal o un tipo que implemente Deref.
La razón por la que el método deref devuelve una referencia a un valor y que la desreferencia normal fuera de los paréntesis en *(y.deref()) todavía es necesaria tiene que ver con el sistema de propiedad. Si el método deref devolviera el valor directamente en lugar de una referencia al valor, el valor se movería fuera de self. No queremos tomar posesión del valor interno dentro de MyBox<T> en este caso o en la mayoría de los casos en los que usamos el operador de desreferencia.
Tenga en cuenta que el operador * se reemplaza con una llamada al método deref y luego una llamada al operador * solo una vez, cada vez que usamos un * en nuestro código. Debido a que la sustitución del operador * no se recursiva infinitamente, terminamos con datos de tipo i32, que coincide con el 5 en assert_eq! de la Lista 15-9.
Coerciones de Deref Implícitas con Funciones y Métodos
La coerción de Deref convierte una referencia a un tipo que implementa el trato Deref en una referencia a otro tipo. Por ejemplo, la coerción de Deref puede convertir &String en &str porque String implementa el trato Deref de manera que devuelve &str. La coerción de Deref es una facilidad que Rust realiza en los argumentos de funciones y métodos, y solo funciona en tipos que implementan el trato Deref. Ocurre automáticamente cuando pasamos una referencia al valor de un tipo particular como argumento a una función o método que no coincide con el tipo de parámetro en la definición de la función o método. Una secuencia de llamadas al método deref convierte el tipo que proporcionamos en el tipo que el parámetro necesita.
La coerción de Deref se agregó a Rust para que los programadores que escriben llamadas a funciones y métodos no necesiten agregar tantas referencias y desreferencias explícitas con & y *. La característica de coerción de Deref también nos permite escribir más código que puede funcionar tanto para referencias como para punteros inteligentes.
Para ver la coerción de Deref en acción, usemos el tipo MyBox<T> que definimos en la Lista 15-8, así como la implementación de Deref que agregamos en la Lista 15-10. La Lista 15-11 muestra la definición de una función que tiene un parámetro de tipo slice de cadena.
Nombre de archivo: src/main.rs
fn hello(name: &str) {
println!("Hello, {name}!");
}
Lista 15-11: Una función hello que tiene el parámetro name de tipo &str
Podemos llamar a la función hello con un slice de cadena como argumento, como hello("Rust");, por ejemplo. La coerción de Deref hace posible llamar a hello con una referencia al valor de tipo MyBox<String>, como se muestra en la Lista 15-12.
Nombre de archivo: src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
Lista 15-12: Llamando a hello con una referencia a un valor MyBox<String>, lo que funciona debido a la coerción de Deref
Aquí estamos llamando a la función hello con el argumento &m, que es una referencia al valor de un MyBox<String>. Debido a que implementamos el trato Deref en MyBox<T> en la Lista 15-10, Rust puede convertir &MyBox<String> en &String llamando a deref. La biblioteca estándar proporciona una implementación de Deref en String que devuelve un slice de cadena, y esto está en la documentación de la API de Deref. Rust llama a deref nuevamente para convertir el &String en &str, lo que coincide con la definición de la función hello.
Si Rust no implementara la coerción de Deref, tendríamos que escribir el código de la Lista 15-13 en lugar del código de la Lista 15-12 para llamar a hello con un valor de tipo &MyBox<String>.
Nombre de archivo: src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
Lista 15-13: El código que tendríamos que escribir si Rust no tuviera coerción de Deref
La (*m) desreferencia el MyBox<String> en una String. Luego el & y [..] toman un slice de cadena de la String que es igual a toda la cadena para coincidir con la firma de hello. Este código sin coerción de Deref es más difícil de leer, escribir y entender con todos estos símbolos involucrados. La coerción de Deref permite que Rust maneje estas conversiones automáticamente para nosotros.
Cuando el trato Deref se define para los tipos involucrados, Rust analizará los tipos y usará Deref::deref tantas veces como sea necesario para obtener una referencia que coincida con el tipo del parámetro. El número de veces que se necesita insertar Deref::deref se resuelve en tiempo de compilación, por lo que no hay penalización en tiempo de ejecución para aprovechar la coerción de Deref.
Cómo la Coerción de Deref Interactúa con la Mutabilidad
De manera similar a cómo se utiliza el trato Deref para sobrescribir el operador * en referencias inmutables, se puede usar el trato DerefMut para sobrescribir el operador * en referencias mutables.
Rust realiza la coerción de Deref cuando encuentra tipos e implementaciones de tratos en tres casos:
- De
&Ta&UcuandoT: Deref<Target=U> - De
&mut Ta&mut UcuandoT: DerefMut<Target=U> - De
&mut Ta&UcuandoT: Deref<Target=U>
Los primeros dos casos son iguales, excepto que el segundo implementa la mutabilidad. El primer caso establece que si tienes una &T, y T implementa Deref a algún tipo U, puedes obtener una &U de manera transparente. El segundo caso establece que la misma coerción de Deref ocurre para referencias mutables.
El tercer caso es más complicado: Rust también coercionará una referencia mutable a una inmutable. Pero lo contrario no es posible: las referencias inmutables nunca se coercerán a referencias mutables. Debido a las reglas de préstamo, si tienes una referencia mutable, esa referencia mutable debe ser la única referencia a esos datos (de lo contrario, el programa no se compilaría). Convertir una referencia mutable en una referencia inmutable nunca romperá las reglas de préstamo. Convertir una referencia inmutable en una referencia mutable requeriría que la referencia inmutable inicial fuera la única referencia inmutable a esos datos, pero las reglas de préstamo no garantizan eso. Por lo tanto, Rust no puede hacer la suposición de que es posible convertir una referencia inmutable en una referencia mutable.
Resumen
¡Felicitaciones! Has completado el laboratorio de Tratamiento de Punteros Inteligentes Como Referencias Regulares con Deref. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.