Ir al contenido

Traits en Rust

Un trait (rasgo) define un conjunto de comportamientos que un tipo puede implementar; se parece un poco a una interfaz de otros lenguajes. Es la forma que tiene Rust de decir “este tipo sabe hacer X”, y es lo que hace funcionar los trait bounds que viste en la página de Genéricos.

trait Speak {
fn speak(&self) -> String;
}

Esto dice: cualquier tipo que implemente Speak tiene que ofrecer un método speak que devuelva un String. Por sí solo, un trait es solo una promesa: no hace nada hasta que algún tipo lo implementa.

trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String {
String::from("¡Guau!")
}
}
impl Speak for Cat {
fn speak(&self) -> String {
String::from("¡Miau!")
}
}
fn main() {
let dog = Dog;
let cat = Cat;
println!("{}", dog.speak());
println!("{}", cat.speak());
}

Salida:

¡Guau!
¡Miau!
▶ Pruébalo tú mismo

Dog y Cat son tipos que no tienen nada que ver entre sí, pero los dos implementan Speak, así que en los dos puedes llamar a .speak(). Es el mismo patrón que los bloques impl de la página de Structs: impl Speak for Dog solo dice “así es como Dog cumple el trait Speak”.

Aquí está la verdadera ventaja: puedes escribir una función que acepte cualquier tipo que implemente un trait, sin importar qué tipo concreto sea:

trait Speak {
fn speak(&self) -> String;
}
struct Dog;
impl Speak for Dog {
fn speak(&self) -> String { String::from("¡Guau!") }
}
fn announce(animal: &impl Speak) {
println!("El animal dice: {}", animal.speak());
}
fn main() {
let dog = Dog;
announce(&dog);
}
▶ Pruébalo tú mismo

animal: &impl Speak significa “cualquier tipo que implemente Speak; no necesito saber cuál”. Si añades un tipo Bird que también implemente Speak, announce funcionará con él sin cambiar nada.

¿Recuerdas #[derive(Debug)] de la página de Structs? Debug es un trait: derive simplemente genera la implementación por ti, en lugar de que la escribas a mano. Otros traits habituales que te irás encontrando: Clone (para .clone()), PartialEq (para ==) y PartialOrd (para < y >, que viste como trait bound en la página de Genéricos).

Un trait puede dar un cuerpo por defecto a un método. Los tipos que lo implementan pueden usarlo tal cual o sobrescribirlo:

trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("¡Hola, {}!", self.name())
}
}

Cualquier tipo que implemente Greet solo tiene que definir name(); greet() le viene de regalo, a menos que decida sobrescribirlo también.

Los traits dan para mucho más de lo que cubre esta página. En Traits comunes repasamos los traits estándar que usarás a diario, y en Traits avanzados verás los tipos asociados, los supertraits y otras herramientas que aparecen en proyectos más grandes.

Define un trait Area con un solo método, fn area(&self) -> f64. Impleméntalo para un Rectangle (con width y height) y para un Square (con side). Imprime el área de un rectángulo de 3 por 4 y la de un cuadrado de lado 5. Pista: cada bloque impl Area for ... solo necesita ese método, y su cuerpo es una simple multiplicación.

Ver solución
trait Area {
fn area(&self) -> f64;
}
struct Rectangle {
width: f64,
height: f64,
}
struct Square {
side: f64,
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
impl Area for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn main() {
let rect = Rectangle { width: 3.0, height: 4.0 };
let square = Square { side: 5.0 };
println!("Rectángulo: {}", rect.area());
println!("Cuadrado: {}", square.area());
}

Salida:

Rectángulo: 12
Cuadrado: 25
▶ Pruébalo tú mismo

Define un trait Describe con fn describe(&self) -> String. Impleméntalo para un Book (con un title) y para un Movie (con un title y minutes). Después escribe una sola función fn show(item: &impl Describe) que imprima la descripción, y llámala con los dos. Pista: usa format! para construir cada cadena de texto.

Ver solución
trait Describe {
fn describe(&self) -> String;
}
struct Book {
title: String,
}
struct Movie {
title: String,
minutes: u32,
}
impl Describe for Book {
fn describe(&self) -> String {
format!("Libro: {}", self.title)
}
}
impl Describe for Movie {
fn describe(&self) -> String {
format!("Película: {} ({} min)", self.title, self.minutes)
}
}
fn show(item: &impl Describe) {
println!("{}", item.describe());
}
fn main() {
let book = Book { title: String::from("El hobbit") };
let movie = Movie { title: String::from("Up"), minutes: 96 };
show(&book);
show(&movie);
}

Salida:

Libro: El hobbit
Película: Up (96 min)
▶ Pruébalo tú mismo

3. Usar o sobrescribir el método por defecto

Sección titulada «3. Usar o sobrescribir el método por defecto»

Toma el trait Greet de esta página, con su método greet() por defecto. Impleméntalo para un Person, que se queda con el método por defecto, y para un Pirate, que sobrescribe greet() para decir ¡Ahoy, ...!. Pista: los dos tipos tienen que definir name(). Solo Pirate escribe además su propio greet().

Ver solución
trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("¡Hola, {}!", self.name())
}
}
struct Person {
name: String,
}
struct Pirate {
name: String,
}
impl Greet for Person {
fn name(&self) -> String {
self.name.clone()
}
}
impl Greet for Pirate {
fn name(&self) -> String {
self.name.clone()
}
fn greet(&self) -> String {
format!("¡Ahoy, {}!", self.name())
}
}
fn main() {
let ana = Person { name: String::from("Ana") };
let jack = Pirate { name: String::from("Jack") };
println!("{}", ana.greet());
println!("{}", jack.greet());
}

Salida:

¡Hola, Ana!
¡Ahoy, Jack!
▶ Pruébalo tú mismo
  • Un trait define un comportamiento que un tipo puede implementar, parecido a una interfaz.
  • impl NombreDelTrait for Tipo aporta la implementación.
  • Usar &impl NombreDelTrait como tipo de un parámetro acepta cualquier tipo que implemente ese trait.
  • Los traits pueden incluir cuerpos de métodos por defecto, que quien los implementa puede usar o sobrescribir.
  • Debug, Clone y PartialEq son traits habituales de la biblioteca estándar; #[derive(...)] los implementa automáticamente.

Comprueba lo aprendido

1. ¿Qué define un trait?

2. ¿Qué significa animal: &impl Speak como tipo de un parámetro?

3. ¿Qué hace realmente #[derive(Debug)]?

4. ¿Qué es una implementación por defecto en un trait?

5. Según esta página, ¿cuál de estos es un trait, y no una struct o un enum?

Puntuación: 0 / 5