Traits e impl blocks
¿Qué es un trait?
En el capítulo anterior, viste cómo los enums permiten que un solo tipo tenga múltiples variantes. Los traits resuelven el problema opuesto: permitir que múltiples tipos compartan una interfaz común.
Un trait es un contrato. Dice "cualquier tipo que implemente este trait debe proporcionar estas funciones." Piensa en él como una descripción de puesto — lista lo que el trabajo requiere, pero diferentes personas (tipos) cumplen esos requisitos de maneras diferentes.
Tu primer trait
trait Describible { fn describir(self) -> String }
Este trait dice: "cualquier tipo que sea Describible debe tener un método describir que tome a sí mismo y devuelva un String."
El parámetro self es especial — se refiere al valor sobre el cual se llama el método.
Implementar un trait
Usa impl NombreTrait for NombreTipo para cumplir el contrato:
trait Describible { fn describir(self) -> String } struct Perro { nombre: String, raza: String } struct Auto { marca: String, anio: int } impl Describible for Perro { fn describir(self) -> String { return self.nombre + " el " + self.raza } } impl Describible for Auto { fn describir(self) -> String { return int_to_string(self.anio) + " " + self.marca } } fn main() { let d: Perro = Perro { nombre: "Rex", raza: "Pastor Alemán" } let c: Auto = Auto { marca: "Toyota", anio: 2024 } print(d.describir()) // Rex el Pastor Alemán print(c.describir()) // 2024 Toyota }
Tanto Perro como Auto implementan Describible, pero cada uno proporciona su propia versión de describir. El método se llama con sintaxis de punto: d.describir().
Métodos sin traits: bloques impl
No siempre necesitas un trait para agregar métodos a un tipo. Un bloque impl simple agrega métodos directamente:
struct Rectangulo { ancho: int, alto: int } impl Rectangulo { fn area(self) -> int { return self.ancho * self.alto } fn perimetro(self) -> int { return 2 * (self.ancho + self.alto) } fn es_cuadrado(self) -> bool { return self.ancho == self.alto } fn escalar(self, factor: int) -> Rectangulo { return Rectangulo { ancho: self.ancho * factor, alto: self.alto * factor } } } fn main() { let r: Rectangulo = Rectangulo { ancho: 10, alto: 5 } print(r.area()) // 50 print(r.perimetro()) // 30 print(r.es_cuadrado()) // false let grande: Rectangulo = r.escalar(3) print(grande.area()) // 450 }
Los métodos en bloques impl se llaman con sintaxis de punto, igual que los métodos de traits. El primer parámetro siempre es self.
Múltiples traits en un tipo
Un tipo puede implementar tantos traits como necesite:
trait Display { fn to_string(self) -> String } trait Eq { fn equals(self, other: Self) -> bool } struct Punto { x: int, y: int } impl Display for Punto { fn to_string(self) -> String { return "(" + int_to_string(self.x) + ", " + int_to_string(self.y) + ")" } } impl Eq for Punto { fn equals(self, other: Punto) -> bool { return self.x == other.x and self.y == other.y } } impl Punto { fn distancia_cuadrada(self, other: Punto) -> int { let dx: int = self.x - other.x let dy: int = self.y - other.y return dx * dx + dy * dy } } fn main() { let a: Punto = Punto { x: 3, y: 4 } let b: Punto = Punto { x: 3, y: 4 } let c: Punto = Punto { x: 1, y: 1 } print(a.to_string()) // (3, 4) print(a.equals(b)) // true print(a.equals(c)) // false print(a.distancia_cuadrada(c)) // 13 }
Traits como parámetros de funciones
Puedes escribir funciones que acepten cualquier tipo que implemente un trait:
trait Display { fn to_string(self) -> String } struct Persona { nombre: String, edad: int } struct Producto { nombre: String, precio: int } impl Display for Persona { fn to_string(self) -> String { return self.nombre + " (edad " + int_to_string(self.edad) + ")" } } impl Display for Producto { fn to_string(self) -> String { return self.nombre + " - $" + int_to_string(self.precio) } } fn imprimir_item<T: Display>(item: T) { print("Item: " + item.to_string()) } fn main() { let p: Persona = Persona { nombre: "Alice", edad: 30 } let prod: Producto = Producto { nombre: "Laptop", precio: 999 } imprimir_item(p) // Item: Alice (edad 30) imprimir_item(prod) // Item: Laptop - $999 }
La sintaxis significa "T puede ser cualquier tipo, siempre que implemente Display." Esto se llama un trait bound (restricción de trait).
Ejemplo práctico: un sistema de formas
trait Forma { fn area(self) -> int fn nombre(self) -> String } struct Circulo { radio: int } struct Cuadrado { lado: int } struct Triangulo { base: int, altura: int } impl Forma for Circulo { fn area(self) -> int { return 3 * self.radio * self.radio } fn nombre(self) -> String { return "Círculo" } } impl Forma for Cuadrado { fn area(self) -> int { return self.lado * self.lado } fn nombre(self) -> String { return "Cuadrado" } } impl Forma for Triangulo { fn area(self) -> int { return self.base * self.altura / 2 } fn nombre(self) -> String { return "Triángulo" } } fn imprimir_forma<T: Forma>(s: T) { print(s.nombre() + " tiene área " + int_to_string(s.area())) } fn main() { let c: Circulo = Circulo { radio: 10 } let cu: Cuadrado = Cuadrado { lado: 7 } let t: Triangulo = Triangulo { base: 6, altura: 8 } imprimir_forma(c) // Círculo tiene área 300 imprimir_forma(cu) // Cuadrado tiene área 49 imprimir_forma(t) // Triángulo tiene área 24 }
Ejemplo práctico: un sistema de puntuación
trait Puntuable { fn puntaje(self) -> int fn etiqueta(self) -> String } struct Estudiante { nombre: String, promedio: int } struct Equipo { nombre: String, victorias: int, derrotas: int } impl Puntuable for Estudiante { fn puntaje(self) -> int { return self.promedio } fn etiqueta(self) -> String { return "Estudiante " + self.nombre } } impl Puntuable for Equipo { fn puntaje(self) -> int { return self.victorias * 3 } fn etiqueta(self) -> String { return "Equipo " + self.nombre } } fn imprimir_ranking<T: Puntuable>(item: T) { print(item.etiqueta() + ": " + int_to_string(item.puntaje()) + " puntos") } fn main() { let s: Estudiante = Estudiante { nombre: "Alice", promedio: 95 } let t: Equipo = Equipo { nombre: "Nyx FC", victorias: 8, derrotas: 2 } imprimir_ranking(s) // Estudiante Alice: 95 puntos imprimir_ranking(t) // Equipo Nyx FC: 24 puntos }
Ejercicios
- Define un trait
Areacon métodofn area(self) -> int. Impleméntalo paraCirculo(radio) yRectangulo(ancho, alto). Escribe una función que imprima el área de cualquier implementador deArea.
- Define un trait
Imprimibleconfn mostrar(self) -> String. Impleméntalo para tres structs diferentes de tu elección.
- Crea un
struct Contador { valor: int }con un bloqueimplque tenga métodos:incrementar(self) -> Contador,decrementar(self) -> Contador,es_cero(self) -> bool.
- Define traits
Nombrado(confn nombre(self) -> String) yConEdad(confn edad(self) -> int). Implementa ambos para un structPersona.
- Construye un mini reino animal: trait
Animalconfn hablar(self) -> Stringyfn patas(self) -> int. Impleméntalo paraPerro,GatoyArana. Imprime un informe de cada uno.
Resumen
- Un
traitdefine un conjunto de métodos que un tipo debe implementar. impl Trait for Tipoproporciona la implementación.impl Tipoagrega métodos directamente sin un trait.- Los métodos usan
selfcomo primer parámetro y se llaman con sintaxis de punto. - Los trait bounds (
) permiten que las funciones acepten cualquier tipo con ese trait. - Un tipo puede implementar múltiples traits.
- Los traits separan el "qué" (interfaz) del "cómo" (implementación).
Siguiente capítulo: Generics →