Uso de límites de rasgos y funciones genéricas

Completado

Los rasgos nos permiten aceptar diferentes tipos por cómo definimos nuestras funciones, porque cuando un tipo implementa un rasgo, puede tratarse de forma abstracta como ese rasgo.

Podemos declarar argumentos de función como un parámetro de tipo anónimo, donde el destinatario debe proporcionar un tipo que tenga los límites declarados por el parámetro de tipo anónimo.

Supongamos que estamos escribiendo una aplicación web y queremos tener una interfaz para serializar valores en el formato JSON. Podríamos escribir un rasgo similar al siguiente:

trait AsJson {
    fn as_json(&self) -> String;
}

Después, podríamos escribir una función que acepte cualquier tipo que implemente el rasgo AsJson. Se escriben como impl seguido de un conjunto de límites de rasgo.

fn send_data_as_json(value: &impl AsJson) {
    println!("Sending JSON data to server...");
    println!("-> {}", value.as_json());
    println!("Done!\n");
}

Aquí, se especifica el nombre del rasgo y la palabra clave impl. Usaremos estos valores en la especificación en lugar de un tipo concreto para el parámetro value. El parámetro value acepta todos los tipos que usan el rasgo definido. Dado que la función no sabe nada sobre el tipo concreto que recibirá, solo puede usar los métodos disponibles por los límites de rasgo del parámetro de tipo anónimo.

Otra manera de escribir la misma función, pero con una sintaxis algo distinta, indica explícitamente que T es un tipo genérico que debe implementar el rasgo AsJson:

fn send_data_as_json<T: AsJson>(value: &T) { ... }

Podemos entonces declarar nuestros tipos e implementar el rasgo AsJson para ellos:

struct Person {
    name: String,
    age: u8,
    favorite_fruit: String,
}

struct Dog {
    name: String,
    color: String,
    likes_petting: bool,
}

impl AsJson for Person {
    fn as_json(&self) -> String {
	    format!(
	        r#"{{ "type": "person", "name": "{}", "age": {}, "favoriteFruit": "{}" }}"#,
	        self.name, self.age, self.favorite_fruit
	    )
    }
}

impl AsJson for Dog {
    fn as_json(&self) -> String {
	    format!(
	        r#"{{ "type": "dog", "name": "{}", "color": "{}", "likesPetting": {} }}"#,
	        self.name, self.color, self.likes_petting
	    )
    }
}

Ahora que tanto Person como Dog implementan el rasgo AsJson, se pueden usar como parámetros de entrada para la función send_data_as_json.

fn main() {
    let laura = Person {
    	name: String::from("Laura"),
	    age: 31,
	    favorite_fruit: String::from("apples"),
    };

    let fido = Dog {
	    name: String::from("Fido"),
	    color: String::from("Black"),
	    likes_petting: true,
    };

    send_data_as_json(&laura);
    send_data_as_json(&fido);
}

¿Pero qué ocurre cuando pasamos a la función un tipo que no implementa el rasgo esperado? Vamos a crear otra estructura y ver lo que sucede:

struct Cat {
    name: String,
    sharp_claws: bool,
}

let kitty = Cat {
    name: String::from("Kitty"),
    sharp_claws: false,
};

send_data_as_json(&kitty);

El compilador genera el siguiente error:

    error[E0277]: the trait bound `Cat: AsJson` is not satisfied
      --> src/main.rs:70:23
       |
    5  | fn send_data_as_json(value: &impl AsJson) {
       |                                   ------ required by this bound in `send_data_as_json`
    ...
    70 |     send_data_as_json(&kitty);
       |                       ^^^^^^ the trait `AsJson` is not implemented for `Cat`

Este error se produjo porque se intentó usar un tipo que no implementa el rasgo AsJson en un lugar que esperaba ese rasgo: la función send_data_as_json.

Para ver el código que se usa en esta unidad, consulte este vínculo al área de juegos de Rust.

Como desafío opcional, puede intentar implementar el rasgo AsJson para el tipo Cat.