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: Copyes 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());
}
1

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.