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.
Definir un trait
Sección titulada «Definir un trait»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.
Implementar un trait
Sección titulada «Implementar un trait»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!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”.
Traits como parámetros de funciones
Sección titulada «Traits como parámetros de funciones»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);}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.
Traits que ya has usado
Sección titulada «Traits que ya has usado»¿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).
Implementaciones por defecto
Sección titulada «Implementaciones por defecto»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.
Práctica
Sección titulada «Práctica»1. Figuras con área
Sección titulada «1. Figuras con área»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: 12Cuadrado: 252. Una función para muchos tipos
Sección titulada «2. Una función para muchos tipos»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 hobbitPelícula: Up (96 min)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!Resumen
Sección titulada «Resumen»- Un trait define un comportamiento que un tipo puede implementar, parecido a una interfaz.
impl NombreDelTrait for Tipoaporta la implementación.- Usar
&impl NombreDelTraitcomo 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,CloneyPartialEqson traits habituales de la biblioteca estándar;#[derive(...)]los implementa automáticamente.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»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
Para leer más
Sección titulada «Para leer más»- Traits: definir comportamiento compartido — El libro de Rust (en inglés): el capítulo oficial sobre cómo definir traits, métodos por defecto, parámetros
impl Traity trait bounds. - Traits — Rust by Example (en inglés): un ejemplo que puedes ejecutar, con un trait, un método por defecto y una implementación.
- Derive — Rust by Example (en inglés): qué traits estándar puede implementar
#[derive(...)]por ti, con ejemplos. - Traits avanzados — El libro de Rust (en inglés): el siguiente paso, con tipos asociados, supertraits y el patrón newtype.
- Traits — The Rust Reference (en inglés): las reglas precisas para definir traits, para cuando quieras todos los detalles.