Traits (Rasgos)
Anteriormente, se han visto algunos traits1 (rasgos). Debug
, Copy
y Clone
son traits (rasgos). Para que un tipo tenga un trait (rasgo) hay que implementarlo. Puesto que algunos traits son tan comunes (como Debug
), existen atributos en Rust que los implementan automáticamente (con una implementación por defecto). Esto es lo que sucede cuando se escribe #[derive(Debug)]
: se implementa de forma automática el trait Debug
.
#[derive(Debug)] struct MiStruct { numero: usize, } fn main() {}
Pero hay otros traits que son más difíciles de implementar y hay que hacerlo a mano con impl
. Por ejemplo, el trait Add
, que se encuentra en std::ops::Add
y se utiliza para sumar dos cosas. Pero Rust no puede adivinar cómo se pueden sumar dos cosas cualquiera, por lo que hay qué codificarlo.
struct CosasASumar { primera_cosa: u32, segunda_cosa: f32, } fn main() {}
Se pueden sumar primera_cosa
y segunda_cosa
, pero hay que dar más información. Puede que se quiera sumar f32
, algo así:
#![allow(unused)] fn main() { // 🚧 let resultado = self.segunda_cosa + self.primera_cosa as f32 }
O puede que se quiera poner self.primera_cosa
junto a self.segunda_cosa
y que sea así como se quiera sumar. Así la suma de 55 a 33.4 sería 5533.4 y no 88.4.
A continuación, se analiza en primer lugar como se crea un trait. Lo importante es recordar que los trait sirven para describir un comportamiento determinado de quien los implementen. Para crear un trait, se escribe trait
y se crean algunas funciones (o ninguna).
struct Animal { // Un simple struct - un Animal que solo contiene su nombre nombre: String, } trait Perro { // El trait Perro asigna alguna funcionalidad fn ladrar(&self) { // Puede ladrar println!("¡Guau, guau!"); } fn correr(&self) { // y puede correr println!("¡El perro está corriendo!"); } } impl Perro for Animal {} // Ahora el Animal implementa el trait Perro fn main() { let rover = Animal { nombre: "Rover".to_string(), }; rover.ladrar(); // El Animal puede usar ladrar() rover.correr(); // y puede usar correr() }
Así está bien, pero no se quiere imprimir "¡El perro está corriendo!". Se pueden modificar los métodos que implementa por defecto un trait, para ello la nueva implementación tiene que tener la misma declaración. Esto significa que tiene que tomar los mismos parámetros y devolver el mismo tipo de resultado. Por ejemplo, se puede modificar el método .correr()
. La declaración indica:
#![allow(unused)] fn main() { // 🚧 fn correr(&self) { println!("¡El perro está corriendo!"); } }
fn correr(&self)
significa que la función correr()
tiene de parámetro &self
y no devuelve ningún valor. Por lo que no se puede definir una nueva implementación así:
#![allow(unused)] fn main() { fn correr(&self) -> i32 { // ⚠️ 5 } }
Rust se quejará así:
= note: expected fn pointer `fn(&Animal)`
found fn pointer `fn(&Animal) -> i32`
Pero sí se puede hacer esto:
struct Animal { // Un simple struct - un Animal que solo contiene su nombre nombre: String, } trait Perro { // El trait Perro asigna alguna funcionalidad fn ladrar(&self) { // Puede ladrar println!("¡Guau, guau!"); } fn correr(&self) { // y puede correr println!("¡El perro está corriendo!"); } } impl Perro for Animal { // Ahora el Animal implementa el trait Perro fn correr(&self) { println!("¡{} está corriendo!", self.nombre); } } fn main() { let rover = Animal { nombre: "Rover".to_string(), }; rover.ladrar(); // El Animal puede usar ladrar() rover.correr(); // y puede usar correr() }
Ahora imprime ¡Rover está corriendo!
. Esta función es correcta porque devuelve ()
o nada, que es lo que indica la implementación del trait.
Cuando se define un trait, se puede escribir solo la declaración de las funciones. Esto obliga a que quien lo quiera implementar tenga que escribir el código que desea para las funciones que solo están declaradas en el trait (sin implementar). En el código siguiente el trait solo declara las funciones sin aportar una definición por defecto de ladrar()
y correr()
, por eso se obliga a escribir el código en la implementación del trait por parte del Perro.
struct Animal { nombre: String, } trait Perro { fn ladrar(&self); // Solo se indica que necesita el parámetro &self y que no devuelva nada fn correr(&self); // necesita &self y que no devuelva nada // Ahora se tiene que escribir el código en la implementación del Perro } impl Perro for Animal { fn ladrar(&self) { println!("¡{}, para de ladrar!", self.nombre); } fn correr(&self) { println!("¡{} está corriendo!", self.nombre); } } fn main() { let rover = Animal { nombre: "Rover".to_string(), }; rover.ladrar(); rover.correr(); }
Cuando se crea un trait (rasgo), se debe pensar: ¿Qué funciones deberá tener? ¿qué funciones se pueden implementar en el propio trait? y ¿qué funciones deberá implementar el propio usuario?
A continuación se implementa el trait Display
para el siguiente struct simple:
struct Gato { nombre: String, edad: u8, } fn main() { let mr_mantle = Gato { nombre: "Reggie Mantle".to_string(), edad: 4, }; }
Se quiere imprimir mr_mantle
. Debug
es fácil de derivar:
#[derive(Debug)] struct Gato { nombre: String, edad: u8, } fn main() { let mr_mantle = Gato { nombre: "Reggie Mantle".to_string(), edad: 4, }; println!("Mr. Mantle es un {:?}", mr_mantle); }
Pero la implementación de Debug no es muy bonita. Este es el resultado:
Mr. Mantle es un Gato { nombre: "Reggie Mantle", edad: 4 }
Por eso, se tiene que implementar Display
para un Gato
si se quiere que la impresión sea bonita. En https://doc.rust-lang.org/std/fmt/trait.Display.html se puede ver la información detallada para Display con un ejemplo que dice:
use std::fmt; struct Position { longitude: f32, latitude: f32, } impl fmt::Display for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "({}, {})", self.longitude, self.latitude) } } fn main() {}
Hay algunas cosas que aún no se entienden en este código, como <'_>
y lo que hace f
. Pero se entiende que el struct Position
tiene únicamente dos valores f32
. Los campos de este struct son self.longitude
y self.latitude
. Así que posiblemente se pueda utilizar este código ara implementar la versión que se necesita para Gato
, cambiando los campos por self.nombre
y self.edad
. Por último, write!
se parece mucho a println!
. Así, el código queda:
use std::fmt; struct Gato { nombre: String, edad: u8, } impl fmt::Display for Gato { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} es un gato de {} años.", self.nombre, self.edad) } } fn main() {}
Si ahora se añade una función fn main()
, el código queda así:
use std::fmt; struct Gato { nombre: String, edad: u8, } impl fmt::Display for Gato { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} es un gato de {} años.", self.nombre, self.edad) } } fn main() { let mr_mantle = Gato { nombre: "Reggie Mantle".to_string(), edad: 4, }; println!("{}", mr_mantle); }
¡Estupendo! Ahora, cuando se usa {}
para imprimir a un Gato, se obtiene Reggie Mantle es un gato de 4 años.
, que queda mucho mejor.
Por cierto, cuando se implementa el trait Display
se dispone del trait ToString
sin nada que hacer adicionalmente. Esto pasa al usar la macro format!
que facilita la creación de un String
con la función .to_string()
. Así que se puede hacer algo como lo siguiente cuando se pasa la variable reggie_mantle
a una función que necesite un String
:
use std::fmt; struct Gato { nombre: String, edad: u8, } impl fmt::Display for Gato { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} es un gato de {} años.", self.nombre, self.edad) } } fn print_gatos(mascota: String) { println!("{}", mascota); } fn main() { let mr_mantle = Gato { nombre: "Reggie Mantle".to_string(), edad: 4, }; print_gatos(mr_mantle.to_string()); // Lo convierte en un String println!("La cadena de caracteres de Mr. Mantle tiene {} letras.", mr_mantle.to_string().chars().count()); // Los convierte en caracteres y los cuenta }
Lo anterior imprime:
Reggie Mantle es un gato de 4 años.
La cadena de caracteres de Mr. Mantle tiene 35 letras.
Lo que se debe recordar sobre los rasgos es que tratan sobre el comportamiento de algo. ¿Cómo se comporta un determinado struct
? ¿Qué puede hacer? Para eso son los rasgos. Si se revisan los rasgos que se han visto hasta el momento, todos tratan sobre algún comportamiento: Copy
es algo que un tipo determinado puede hacer. Display
también es algo que un tipo puede hacer. ToString
es otro rasgo y también es algo que un tipo puede hacer: convertirse en una String
. En el rasgo Perro
la palabra perro no significa algo que se pueda hacer, pero proporciona algunos métodos que le permiten hacer cosas. Se puede implementar para una estructura Caniche
o para una Chucho
y así ambas podrían disponer de los métodos de Perro
.
A continuación se presenta otro ejemplo aún más conectado con algo que solo es comportamiento. Se imaginará un juego de fantasía con algunos personajes simples. Uno es un Monstruo
, los otros dos son Mago
y Cazador
. El Monstruo
tiene salud
y se le puede atacar, los otros dos no tendrán nada aún. Se crearán dos rasgos (traits). Uno llamado LuchaCercana
que permite luchar de cerca y otro LuchaADistancia
que permite lugar desde lejos. Solo el Cazador
puede usar LuchaADistancia
. Este es el código resultante:
struct Monstruo { salud: i32, } struct Mago {} struct Cazador {} trait LuchaCercana { fn atacar_con_espada(&self, oponente: &mut Monstruo) { oponente.salud -= 10; println!( "Atacaste con espada. Tu oponente tiene ahora {} de salud.", oponente.salud ); } fn atacar_con_la_mano(&self, oponente: &mut Monstruo) { oponente.salud -= 2; println!( "Atacaste con la mano. tu oponente tiene ahora {} de salud.", oponente.salud ); } } impl LuchaCercana for Mago {} impl LuchaCercana for Cazador {} trait LuchaADistancia { fn atacar_con_arco(&self, oponente: &mut Monstruo, distancia: u32) { if distancia < 10 { oponente.salud -= 10; println!( "Atacaste con el arco. Tu oponente tiene ahora {} de salud.", oponente.salud ); } } fn atacar_con_piedra(&self, oponente: &mut Monstruo, distancia: u32) { if distancia < 3 { oponente.salud -= 4; } println!( "Atacaste con una piedra. Your oponente tiene ahora {} de salud.", oponente.salud ); } } impl LuchaADistancia for Cazador {} fn main() { let radagast = Mago {}; let aragorn = Cazador {}; let mut uruk_hai = Monstruo { salud: 40 }; radagast.atacar_con_espada(&mut uruk_hai); aragorn.atacar_con_arco(&mut uruk_hai, 8); }
Lo anterior imprime:
Atacaste con espada. Tu oponente tiene ahora 30 de salud.
Atacaste con el arco. Tu oponente tiene ahora 20 de salud.
En este ejemplo, se pasa constantemente self
a los métodos de los rasgos, pero solo se conoce que se trata de un tipo que implementa el propio rasgo. Puede ser un Mago
o un Cazador
. O podría ser una nueva struct
llamada Toefocfgetobjtnode
o cualquier otra que implementara el rasgo correspondiente. Para facilitar que self
tenga más funcionalidad, se puede añadir restricciones a un trait
. Se puede añadir otros trait
que son necesarios para el trait
que se define. Por ejemplo, si que se quiere que en los métodos de un trait
se pueda usar {:?}
para imprimir, se necesita que implemente el trait Debug
. Eso se puede indicar mediante la escritura de :
(dos puntos). Así quedaría el código:
struct Monstruo { salud: i32, } #[derive(Debug)] // Ahora el mago tiene Debug struct Mago { salud: i32, // Ahora el mago tiene salud } #[derive(Debug)] // Ahora el Cazador tiene Debug struct Cazador { salud: i32, // Ahora el cazador tiene salud } trait LuchaCercana: std::fmt::Debug { // Ahora el tipo necesita tener Debug // para poder implementar LuchaCercana fn atacar_con_espada(&self, oponente: &mut Monstruo) { oponente.salud -= 10; println!( "Atacaste con espada. Tu oponente tiene ahora {} de salud. Ahora tienes {:?}", // se puede usar {:?} porque se dispone de Debug oponente.salud, &self ); } fn atacar_con_la_mano(&self, oponente: &mut Monstruo) { oponente.salud -= 2; println!( "Atacaste con la mano. tu oponente tiene ahora {} de salud. Ahora tienes {:?}", oponente.salud, &self ); } } impl LuchaCercana for Mago {} impl LuchaCercana for Cazador {} trait LuchaADistancia: std::fmt::Debug { // también se podría haber escrito LuchaADistancia: LuchaCercana // porque LuchaCercana implementa ya Debug fn atacar_con_arco(&self, oponente: &mut Monstruo, distancia: u32) { if distancia < 10 { oponente.salud -= 10; println!( "Atacaste con el arco. Tu oponente tiene ahora {} de salud. Ahora tienes {:?}", oponente.salud, &self ); } } fn atacar_con_piedra(&self, oponente: &mut Monstruo, distancia: u32) { if distancia < 3 { oponente.salud -= 4; } println!( "Atacaste con una piedra. Your oponente tiene ahora {} de salud. Ahora tienes {:?}", oponente.salud, &self ); } } impl LuchaADistancia for Cazador {} fn main() { let radagast = Mago { salud: 60 }; let aragorn = Cazador { salud: 80 }; let mut uruk_hai = Monstruo { salud: 40 }; radagast.atacar_con_espada(&mut uruk_hai); aragorn.atacar_con_arco(&mut uruk_hai, 8); }
Ahora, se imprime:
Atacaste con espada. Tu oponente tiene ahora 30 de salud. Ahora tienes Mago { salud: 60 }
Atacaste con el arco. Tu oponente tiene ahora 20 de salud. Ahora tienes Cazador { salud: 80 }
En un juego real sería mejor reescribir esto para cada tipo, ya que Ahora tienes Mago { salud: 60 }
queda muy raro. Por eso, los métodos predefinidos de los rasgos suelen ser muy simples, porque no se conoce el tipo que lo va a usar. No se puede escribir algo como self.0 += 10
, por ejemplo. En cualquier caso, el ejemplo anterior muestra cómo se puede usar otro trait dentro de uno que se esté escribiendo y disponer así de los métodos de ese otro.
Otra forma de usar un rasgo es mediante lo que se llama trait bounds
(N.T.: límites de un rasgo). Son fáciles de implementar ya que no necesitan nada. A continuación, se reescribe el ejemplo anterior sin que los rasgos implementen ningún método, a cambio, se dispone de funciones que necesitan rasgos para que se puedan usar.
use std::fmt::Debug; // para no tener que escribir todo el camino al rasgo struct Monstruo { salud: i32, } #[derive(Debug)] struct Mago { salud: i32, } #[derive(Debug)] struct Cazador { salud: i32, } trait Magia{} // todos los traits sin métodos. Son límites de rasgo trait LuchaCercana {} trait LuchaADistancia {} impl LuchaCercana for Cazador{} // Cada tipo tiene LuchaCercana, impl LuchaCercana for Mago {} impl LuchaADistancia for Cazador{} // Pero solo el cazador lucha a distancia impl Magia for Mago{} // y solo el mago hace magia fn atacar_con_flecha<T: LuchaADistancia + Debug>(personaje: &T, oponente: &mut Monstruo, distancia: u32) { if distancia < 10 { oponente.salud -= 10; println!( "Atacaste con el arco. Tu oponente tiene ahora {} de salud. Ahora tienes {:?}", oponente.salud, personaje ); } } fn atacar_con_espada<T: LuchaCercana + Debug>(personaje: &T, oponente: &mut Monstruo) { oponente.salud -= 10; println!( "Atacaste con la espada. Tu oponente tiene ahora {} de salud. Ahora tienes {:?}", oponente.salud, personaje ); } fn atacar_con_bola_de_fuego<T: Magia + Debug>(personaje: &T, oponente: &mut Monstruo, distancia: u32) { if distancia < 15 { oponente.salud -= 20; println!("¡Levantas las manos y conjuras una bola de fuego! Tu oponente tiene ahora {} de salud. Ahora tienes: {:?}", oponente.salud, personaje); } } fn main() { let radagast = Mago { salud: 60 }; let aragorn = Cazador { salud: 80 }; let mut uruk_hai = Monstruo { salud: 40 }; atacar_con_espada(&radagast, &mut uruk_hai); atacar_con_flecha(&aragorn, &mut uruk_hai, 8); atacar_con_bola_de_fuego(&radagast, &mut uruk_hai, 8); }
Atacaste con la espada. Tu oponente tiene ahora 30 de salud. Ahora tienes Mago { salud: 60 }
Atacaste con el arco. Tu oponente tiene ahora 20 de salud. Ahora tienes Cazador { salud: 80 }
¡Levantas las manos y conjuras una bola de fuego! Tu oponente tiene ahora 0 de salud. Ahora tienes: Mago { salud: 60 }
Se puede ver que hay varias formas de obtener el mismo resultado cuando se usan rasgos. Todo depende de lo que tenga más sentido en el programa que se está escribiendo.
A continuación, se echa un vistazo a cómo implementar algunos de los rasgos más usados en Rust.
El rasgo From
From es un rasgo muy útil que ya se ha usado. Con From se puede crear una String
a partir de una &str
, pero se puede usar para convertir cualquier tipo en otro. Por ejemplo, Vec
utiliza From para lo siguiente:
From<&'_ [T]>
From<&'_ mut [T]>
From<&'_ str>
From<&'a Vec<T>>
From<[T; N]>
From<BinaryHeap<T>>
From<Box<[T]>>
From<CString>
From<Cow<'a, [T]>>
From<String>
From<Vec<NonZeroU8>>
From<Vec<T>>
From<VecDeque<T>>
Implementa un montón de Vec::from()
que no se han usado aún en este manual. Se pueden usar algunos para ver qué pasa.
use std::fmt::Display; // se usará Display para crear una función genérica y mostrar el resultado fn print_vec<T: Display>(input: &Vec<T>) { // Toma cualquier Vec<T> cuyo tipo T que tenga Display for item in input { print!("{} ", item); } println!(); } fn main() { let array_vec = Vec::from([8, 9, 10]); // "from" un array print_vec(&array_vec); let str_vec = Vec::from("¿Qué clase de vector seré?"); // Array "from" un &str print_vec(&str_vec); let string_vec = Vec::from("Un String ¿Qué clase de vector será?".to_string()); // También "from" un String print_vec(&string_vec); }
Se imprime lo siguiente:
8 9 10
87 104 97 116 32 107 105 110 100 32 111 102 32 118 101 99 32 119 105 108 108 32 73 32 98 101 63
87 104 97 116 32 107 105 110 100 32 111 102 32 118 101 99 32 119 105 108 108 32 97 32 83 116 114 105 110 103 32 98 101 63
Si se observa el tipo, los segundo y tercer vector son de tipo Vec<u8>
, lo que significa que contiene los bytes de &str
y String
. Se ve así que From
es muy flexible y se usa mucho.
El siguiente ejemplo implemente From
en tipos desarrollados en el código. Se construyen dos structs y luego implementan From
para una de ellas. Una será una Ciudad
y la otra un Pais
. Se quiere que sea posible hacer esto: let country_nombre = Pais::from(vector_de_ciudades)
.
Queda así el código:
#[derive(Debug)] // Para poder imprimir la Ciudad struct Ciudad { nombre: String, poblacion: u32, } impl Ciudad { fn new(nombre: &str, poblacion: u32) -> Self { // función new Self { nombre: nombre.to_string(), poblacion, } } } #[derive(Debug)] // Pais necesita imprimirse struct Pais { ciudades: Vec<Ciudad>, // Las ciudades van aquí } impl From<Vec<Ciudad>> for Pais { // Note: no se crea From<Ciudad>, se crea // From<Vec<Ciudad>>. Es decir, sobre un tipo, Vec, que no se ha creado en el código fn from(ciudades: Vec<Ciudad>) -> Self { Self { ciudades } } } impl Pais { fn imprimir_ciudades(&self) { // función para imprimir las ciudades del Pais for ciudad in &self.ciudades { // & porque Vec<Ciudad> no es Copy println!("{:?} tiene una población de {:?}.", ciudad.nombre, ciudad.poblacion); } } } fn main() { let helsinki = Ciudad::new("Helsinki", 631_695); let turku = Ciudad::new("Turku", 186_756); let finland_ciudades = vec![helsinki, turku]; // Esto es el Vec<Ciudad> let finland = Pais::from(finland_ciudades); // En este punto se usa From finland.imprimir_ciudades(); }
Que imprime:
"Helsinki" tiene una población de 631695.
"Turku" tiene una población de 186756.
Se observa que es fácil implementar From
para tipos que no se han creado por el desarrollador, por ejemplo, Vec
, i32
o cualquier otro.
A continuación, se muestra otro ejemplo en el que se crea un vector que contiene dos vectores. El primer vector contiene números pares y el segundo, impares. Con From
se puede tomar un vector de i32
y convertirlo en Vec<Vec<i32>>
: un vector que contiene vectores de i32
.
use std::convert::From; struct ParImparVec(Vec<Vec<i32>>); impl From<Vec<i32>> for ParImparVec { fn from(input: Vec<i32>) -> Self { let mut par_impar_vec: Vec<Vec<i32>> = vec![vec![], vec![]]; // Un vec con dos vecs vacíos dentro // se rellena para después retornarlo for item in input { if item % 2 == 0 { par_impar_vec[0].push(item); } else { par_impar_vec[1].push(item); } } Self(par_impar_vec) // una vez relleno se devuelve como Self (Self = ParImparVec) } } fn main() { let varios_numero = vec![8, 7, -1, 3, 222, 9787, -47, 77, 0, 55, 7, 8]; let new_vec = ParImparVec::from(varios_numero); println!("Números pares: {:?}\nNúmeros impares: {:?}", new_vec.0[0], new_vec.0[1]); }
Lo anterior imprime:
Números pares: [8, 222, 0, 8]
Números impares: [7, -1, 3, 9787, -47, 77, 55, 7]
Un tipo como ParImparVec
es mejor definirlo como genérico T
para poder usarlo en muchos tipos de números. Se deja esta idea como práctica.
Paso de parámetros String y &str a funciones
En ocasiones se necesita que una función pueda recibir tanto String
como &str
en el mismo parámetro. Esto se puede conseguir con genéricos y con el rasgo AsRef
. AsRef
se utiliza para pasar una referencia de un tipo a otro tipo. Si se observa la documentación de String
, se ve que implementa AsRef
para muchos tipos:
https://doc.rust-lang.org/std/string/struct.String.html
Estas son algunas de las declaraciones de función:
AsRef<str>
:
#![allow(unused)] fn main() { // 🚧 impl AsRef<str> for String fn as_ref(&self) -> &str }
AsRef<[u8]>
:
#![allow(unused)] fn main() { // 🚧 impl AsRef<[u8]> for String fn as_ref(&self) -> &[u8] }
AsRef<OsStr>
:
#![allow(unused)] fn main() { // 🚧 impl AsRef<OsStr> for String fn as_ref(&self) -> &OsStr }
Se observa que implementa la función as_ref
que recibe como parámetro &self
y devuelve una referencia a otro tipo. Esto permite que si se dispone de un tipo genérico T, se pueda indicar que ese tipo necesita AsRef<str>
. Así, será posible que se pase como un &str
o un String
.
El código siguiente implementa la función genérica, pero da error:
fn imprimelo<T>(input: T) { println!("{}", input) // ⚠️ } fn main() { imprimelo("Por favor, imprímeme"); }
Rust dice error[E0277]:
T doesn't implement std::fmt::Display
. Se necesita que lo implemente:
use std::fmt::Display; fn imprimelo<T: Display>(input: T) { println!("{}", input) } fn main() { imprimelo("Por favor, imprímeme"); }
Ahora funciona e imprime Por favor, imprímeme
. Eso es mejor, pero T puede ser muchas cosas aún. Puede ser i8
, f32
o cualquier cosa que tenga Display
. Si se añade ahora AsRef<str>
, T necesita ambos rasgos.
use std::fmt::Display; fn imprimelo<T: AsRef<str> + Display>(input: T) { println!("{}", input) } fn main() { imprimelo("Por favor, imprímeme"); imprimelo("También, por favor, imprímeme".to_string()); // imprimelo(7); <- Esto no compilará }
Ahora no dejará pasar tipos como i8
.
No se puede olvidar que se puede utilizar where
ara escribir de forma más compacta la declaración de la función cuando esta se hace muy larga. Si se añadiera Debug la línea quedaría muy larga, fn imprimelo<T: AsRef<str> + Display + Debug>(input: T)
, así que se puede escribir de la siguiente forma.
use std::fmt::{Debug, Display}; // añade Debug fn imprimelo<T>(input: T) // Ahora la línea es más fácil de leer where T: AsRef<str> + Debug + Display, // y también se leen bien los rasgos del tipo { println!("{}", input) } fn main() { imprimelo("Por favor, imprímeme"); imprimelo("También, por favor, imprímeme".to_string()); }
N.T.: Este concepto de Rust se puede traducir como rasgo. Está relacionado con la programación orientada a aspectos. Los rasgos de Rust describen determinados aspectos de los tipos, funciones o variables que los implementen.