Lección 18 de 26 Genéricos, traits y módulos
Traits: comportamiento compartido
- Un trait describe un comportamiento. trait Resumen es un contrato: quien lo cumpla debe tener resumir.
- impl Resumen for Noticia e impl Resumen for Post: cada tipo cumple el contrato a su manera.
- notificar(item: &impl Resumen) acepta cualquier tipo que implemente Resumen: los dos pasan.
- Un tipo sin impl Resumen, como i32, no pasa: el compilador lo rechaza antes de ejecutar nada.
En la lección anterior escribiste T: PartialOrd para decir «cualquier tipo que se pueda comparar». PartialOrd es un trait: un conjunto de métodos que describe un comportamiento. Cualquier tipo puede implementarlo y, desde ese momento, se puede usar allí donde se pida ese comportamiento.
Si vienes de otros lenguajes, los traits se parecen a las interfaces de Java, TypeScript o Go. Son la pieza que une todo en Rust: genéricos, Display, Clone, iteradores, closures…
Definir un trait
Con trait declaras un nombre y las firmas de sus métodos, sin cuerpo:
trait Resumen {
fn resumir(&self) -> String;
}Cualquier tipo que implemente Resumen tendrá que aportar su propio método resumir.
Implementar un trait
Con impl Trait for Tipo escribes la implementación para un tipo concreto:
trait Resumen {
fn resumir(&self) -> String;
}
struct Noticia {
titular: String,
autor: String,
}
struct Post {
usuario: String,
texto: String,
}
impl Resumen for Noticia {
fn resumir(&self) -> String {
format!("{}, por {}", self.titular, self.autor)
}
}
impl Resumen for Post {
fn resumir(&self) -> String {
format!("@{}: {}", self.usuario, self.texto)
}
}
fn main() {
let noticia = Noticia {
titular: String::from("Rust 2024 ya está aquí"),
autor: String::from("Ferris"),
};
let post = Post {
usuario: String::from("midudev"),
texto: String::from("Aprendiendo traits"),
};
println!("{}", noticia.resumir());
println!("{}", post.resumir());
}Cada tipo resume a su manera, pero los dos comparten el mismo método. Hay una regla: para implementar un trait en un tipo, el trait o el tipo tienen que ser tuyos. Puedes implementar Display para tu Noticia, pero no para Vec<T>, porque ni el trait ni el tipo son de tu crate.
Métodos por defecto
Un trait puede dar una implementación por defecto. Los tipos la reciben gratis y pueden sobrescribirla si quieren. Además, un método por defecto puede llamar a otros métodos del trait, incluso a los que no tienen cuerpo:
trait Resumen {
fn autor(&self) -> String;
fn resumir(&self) -> String {
format!("(Leer más de {}...)", self.autor())
}
}
struct Post {
usuario: String,
}
struct Noticia {
titular: String,
autor: String,
}
impl Resumen for Post {
fn autor(&self) -> String {
format!("@{}", self.usuario)
}
}
impl Resumen for Noticia {
fn autor(&self) -> String {
self.autor.clone()
}
fn resumir(&self) -> String {
format!("{}, por {}", self.titular, self.autor())
}
}
fn main() {
let post = Post { usuario: String::from("ferris") };
let noticia = Noticia {
titular: String::from("Rust 2024 ya está aquí"),
autor: String::from("Ferris"),
};
println!("{}", post.resumir()); // usa el método por defecto
println!("{}", noticia.resumir()); // usa el suyo propio
}Traits como parámetros
Lo potente de los traits es escribir funciones que acepten cualquier tipo con un comportamiento. Para ello, usa impl Trait como tipo del parámetro:
fn notificar(item: &impl Resumen) {
println!("¡Novedad! {}", item.resumir());
}Es una forma abreviada de escribir una función genérica con un trait bound, como en la lección anterior. Esta versión es equivalente:
fn notificar<T: Resumen>(item: &T) {
println!("¡Novedad! {}", item.resumir());
}La versión genérica es útil cuando quieres obligar a que dos parámetros sean del mismo tipo: fn comparar<T: Resumen>(a: &T, b: &T). Con a: &impl Resumen, b: &impl Resumen podrían ser tipos distintos.
Varios traits: + y where
Si el tipo tiene que cumplir varios traits, únelos con +. Esta función necesita comparar (PartialOrd) e imprimir (Display):
use std::fmt::Display;
fn mostrar_mayor<T: Display + PartialOrd>(a: T, b: T) {
if a >= b {
println!("El mayor es {a}");
} else {
println!("El mayor es {b}");
}
}
fn main() {
mostrar_mayor(7, 3);
mostrar_mayor("pera", "uva");
}Cuando hay muchos bounds, la firma se vuelve difícil de leer. Con where los mueves debajo y la firma queda limpia:
fn mostrar_mayor<T>(a: T, b: T)
where
T: Display + PartialOrd,
{
// ...
}Devolver impl Trait
También puedes usar impl Trait como tipo de retorno: «devuelvo algo que implementa este trait», sin decir qué tipo exacto:
fn crear_resumen() -> impl Resumen {
Post { usuario: String::from("ferris") }
}Ojo: solo funciona si la función devuelve un único tipo concreto. Si según una condición devolviera un Post o una Noticia, no compilaría. Lo verás mucho con closures e iteradores, cuyos tipos son tan largos que es imposible escribirlos.
Display: imprime tus propios tipos
println!("{}", x) funciona con cualquier tipo que implemente el trait std::fmt::Display. Ya lo usaste con tus errores; ahora sabes qué significa ese impl ... for:
use std::fmt;
struct Punto {
x: i32,
y: i32,
}
impl fmt::Display for Punto {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Punto { x: 3, y: -2 };
println!("El punto es {p}");
let texto = p.to_string();
println!("Como String: {texto}");
}De regalo, todo tipo que implementa Display tiene el método to_string().
derive: implementaciones automáticas
Muchos traits tienen una implementación obvia: comparar campo a campo, clonar campo a campo… Para esos, #[derive(...)] la escribe por ti:
#[derive(Debug, Clone, PartialEq)]
struct Color {
r: u8,
g: u8,
b: u8,
}
fn main() {
let rojo = Color { r: 255, g: 0, b: 0 };
let copia = rojo.clone();
println!("{rojo:?}");
println!("¿Iguales? {}", rojo == copia);
}
| Trait | Qué te da |
|---|---|
Debug | Imprimir con {:?} |
Clone | Duplicar con .clone() |
Copy | Copia implícita al asignar, como los enteros (requiere Clone) |
PartialEq | Comparar con == y != |
PartialOrd | Comparar con <, >, <= y >= |
Default | Un valor por defecto con Default::default() |
Display no se puede derivar: cómo se muestra un tipo al usuario lo decides tú.
Trait objects: mezclar tipos con dyn
Un Vec<T> solo guarda elementos de un único tipo. ¿Y si quieres una lista de formas con círculos y rectángulos? Con genéricos no puedes, porque T se fija a un tipo concreto. La solución son los trait objects: Box<dyn Forma> significa «una caja con cualquier valor que implemente Forma».
trait Forma {
fn area(&self) -> f64;
fn nombre(&self) -> String;
}
struct Circulo {
radio: f64,
}
struct Rectangulo {
ancho: f64,
alto: f64,
}
impl Forma for Circulo {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radio * self.radio
}
fn nombre(&self) -> String {
String::from("Círculo")
}
}
impl Forma for Rectangulo {
fn area(&self) -> f64 {
self.ancho * self.alto
}
fn nombre(&self) -> String {
String::from("Rectángulo")
}
}
fn main() {
let formas: Vec<Box<dyn Forma>> = vec![
Box::new(Circulo { radio: 1.0 }),
Box::new(Rectangulo { ancho: 2.0, alto: 3.0 }),
Box::new(Circulo { radio: 0.5 }),
];
for forma in &formas {
println!("{}: {:.2}", forma.nombre(), forma.area());
}
}dyn Formaes «algún tipo que implementaForma». Cuál exactamente se descubre en ejecución.Boxguarda el valor en el heap. Hace falta porque un círculo y un rectángulo ocupan tamaños distintos y unVecnecesita elementos del mismo tamaño. Lo verás a fondo en la lección de smart pointers.
Genéricos o dyn: Con genéricos (
impl Trait,<T: Trait>) el compilador genera una versión por tipo y sabe qué método llamar: es lo más rápido. Condyn Traitla decisión se toma en ejecución, con un coste pequeño, a cambio de poder mezclar tipos. Usa genéricos por defecto ydyncuando necesites mezclar.
Hora de practicar
Ejercicios
0/4 resueltos
Reto 1 de 4 Resuelto
Saludar para Persona y para Robot. La persona dice Hola, soy NOMBRE y el robot BIP BOP. UNIDAD ID ACTIVADA.Hola, soy Ana BIP BOP. UNIDAD 42 ACTIVADA
Al ejecutar, unos tests ocultos comprueban tus funciones.
Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.