La primera parte de la definición de una función la podemos llamar la "firma" de una función. La "firma" de la función es el nombre de la función, las entradas de la función y las salidas de la función. Si no nos interesa la implementación (el código) podemos sólo prestar atención en el nombre de la función. De esta forma si le pasamos a la función la(s) entrada(s) necesaria(s) podemos confiar que la salida sea correcta.
En este sentido podemos construir código que confíe en que la implementación es correcta. El requisito será que necesitaremos una función que siga la "firma" requerida por nuestro código. En Java o C# lo anterior lo logramos con el concepto de Interfaz o Interfaces. En Rust tenemos algo parecido que lo llamamos "traits". En Rust en realidad no tenemos "objetos" como los tenemos en Java o C# o en cualquier otro lenguaje orientado a objetos. Así que las "traits" las implementamos dentro de "structs".
En Rust las "traits" son una forma de definir las firmas de funciones/métodos que queremos que tenga implementados un struct. Por ahora sólo crearemos "traits". En la siguiente entrada veremos como "implementar" un "trait" en un struct. Con los "traits" podemos escribir código que confíe en obtener la salida correcta al llamar estos métodos y así ahorrarnos escribir el código.
En programación orientada a objectos podemos usar las variables definidas dentro del objeto. De la misma forma en estas funciones implementadas dentro de un "struct" podemos usar el struct. Ya sea como una referencia de lectura a si mismo ("&self") o una referencia mutable a sí mismo
("&mut self"). En donde usaremos "self" para acceder a los elementos del struct.
En el siguiente ejemplo definimos un struct para tener datos como un vector 2d (x,y) y un enum para guardar si el módulo (o norma) del vector 2d es igual a 1, mayor a 1 o menor a 1. Para simplificar este caso usaremos puros números enteros. Definimos un "trait" (interfaz) que queremos que tenga un objeto para poder hacer operaciones de vectores 2d. En este ejemplo debe poder:
- Dado la estructura contenedora (self), obtener el enum que corresponde con el módulo del vector. La función "modulo_comparado_con_uno". En este caso nosotros nos aseguraremos manualmente que "&self" siempre sea de tipo Vector2d ya que en una entrada futura sólo implementaremos este "trait" dentro de la estructura Vector2d.
- Dado la estructura contenedora (self) y un struct de tipo vector2d determinar si son iguales y regresar un booleano correspondiente al resultado.
- Dado la estructura contenedora (self) y un struct de tipo vector2d determinar el producto punto entre estos dos y regresar el entero resultante.
- Dado la estructura contenedora (self) y un struct de tipo vector2d determinar la suma de los dos vectores y regresar el Vector2d resultante.
- Dado la estructura contenedora (self) y un escalar (entero) calcular el producto escalar y regresar el Vector2d resultante.
Para ver la facilidades que proporciona definir un "trait" también agregué una función llamada "producto_punto_y_luego_escalar". Esta función toma una entrada1 una variable de tipo "&impl OpsVector2d" y otra de tipo "&Vector2d". Lo que hace la definición "&impl OpsVector2d" es que acepte como válidas todo tipo de dato que tenga implementado ("impl") el trait OpsVector2d, así sólo podemos dejar pasar estructuras que tengan OpsVector2d implementado. De la misma forma al definirlo de forma genérica usando "&impl" podemos tomar varios tipos diferentes ya que nunca mencionamos ningún tipo específico, nuestro único requisito es que implemente OpsVector2d. Por otra parte la segunda entrada la definimos como "&Vector2d" así que sólo variables de tipo Vector2d pueden entrar.
Este es el código que muestra lo anterior con algunas variables sin uso (con "_" como prefijo) para evitar warnings:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | struct Vector2d { x: i32, y: i32 } enum ModuloDeVector { MayorAUno, ExactamenteUno, MenorAUno } trait OpsVector2d { fn modulo_comparado_con_uno(&self) -> ModuloDeVector; fn igualdad(&self, v2: &Vector2d) -> bool; fn producto_punto(&self, v2: &Vector2d) -> i32; fn suma(&self, v2: &Vector2d) -> Vector2d; fn producto_escalar(&self, escalar: i32) -> Vector2d; } fn producto_punto_y_luego_escalar( entrada1: &impl OpsVector2d, entrada2: &Vector2d) -> Vector2d { let resultado1 = entrada1.producto_punto(entrada2); entrada1.producto_escalar(resultado1) } fn main() { let _enum_modulo_op1 = ModuloDeVector::MayorAUno; let _enum_modulo_op2 = ModuloDeVector::MenorAUno; let _enum_modulo_op3 = ModuloDeVector::ExactamenteUno; let _mi_vector1: Vector2d = Vector2d { x: 2, y: 3}; let _mi_vector2: Vector2d = Vector2d { x: 1, y: 0}; let _mi_vector3: Vector2d = Vector2d { x: 0, y: 0}; println!("¡El 'trait' está definido fin del programa!"); } |
El resultado de correr el código anterior es:
Como podemos ver no evitamos algunos warnings por código sin utilizar, pero no hay problema, en la próxima entrada intentaremos implementar el trait "OpsVector2d" en el struct "Vector2d" y probaremos nuestro código.
Navegación:
Primera parte
Siguiente parte
Parte anterior
Fuente (inglés):
- Traits, defining shared behavior https://doc.rust-lang.org/book/ch10-02-traits.html
- Traits, Rust by example https://doc.rust-lang.org/rust-by-example/trait.html
No hay comentarios.:
Publicar un comentario