Índice

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

  1. Define un trait Area con método fn area(self) -> int. Impleméntalo para Circulo (radio) y Rectangulo (ancho, alto). Escribe una función que imprima el área de cualquier implementador de Area.
  1. Define un trait Imprimible con fn mostrar(self) -> String. Impleméntalo para tres structs diferentes de tu elección.
  1. Crea un struct Contador { valor: int } con un bloque impl que tenga métodos: incrementar(self) -> Contador, decrementar(self) -> Contador, es_cero(self) -> bool.
  1. Define traits Nombrado (con fn nombre(self) -> String) y ConEdad (con fn edad(self) -> int). Implementa ambos para un struct Persona.
  1. Construye un mini reino animal: trait Animal con fn hablar(self) -> String y fn patas(self) -> int. Impleméntalo para Perro, Gato y Arana. Imprime un informe de cada uno.

Resumen

Siguiente capítulo: Generics →

← Anterior: Enums y pattern matching Siguiente: Generics →