Un trait en Rust es una herramienta que define un comportamiento compartido que diferentes tipos pueden implementar. Puedes imaginar un trait como un contrato: si un tipo dice que implementa un trait específico, garantiza que puede realizar ciertas acciones (métodos) definidas en ese contrato. Esto permite el polimorfismo, permitiéndote escribir código que trabaje con cualquier tipo que cumpla con un comportamiento, sin importar su estructura interna.

Para usar un trait, primero se define con la palabra clave trait, especificando las firmas de los métodos que los tipos deberán proporcionar. Luego, se utiliza la palabra clave impl seguida del nombre del trait y el tipo de dato para realizar la implementación real. Es crucial entender la “Regla de los Huérfanos” (Orphan Rule): para implementar un trait para un tipo, o el trait o el tipo deben estar definidos en tu crate actual. Esto evita conflictos de implementación entre librerías.

Un rasgo poderoso es la “implementación por defecto”. Un trait puede proporcionar un cuerpo de función para un método; si lo hace, los tipos que implementen el trait no estarán obligados a escribir su propia lógica para ese método, aunque pueden sobrescribirlo si desean un comportamiento personalizado. Si intentas implementar un trait sin definir los métodos obligatorios, el compilador de Rust emitirá un error de seguridad.

// Definición del trait Area
trait Area {
    // Método obligatorio: cada tipo debe definir cómo calcular su área
    fn calcular_area(&self) -> f64;

    // Método con implementación por defecto
    fn describir(&self) -> String {
        format!("El área de este objeto es: {}", self.calcular_area())
    }
}

struct Rectangulo {
    base: f64,
    altura: f64,
}

struct Circulo {
    radio: f64,
}

// Implementación del trait Area para el tipo Rectangulo
impl Area for Rectangulo {
    fn calcular_area(&self) -> f64 {
        self.base * self.altura
    }
    // No sobreescribimos describir(), usará la versión por defecto
}

// Implementación del trait Area para el tipo Circulo
impl Area for Circulo {
    fn calcular_area(&self) -> f64 {
        std::f64::consts::PI * self.radio * self.radio
    }

    // Sobrescribimos el método por defecto para dar un formato personalizado
    fn describir(&self) -> String {
        format!("El área circular calculada es: {:.2}", self.calcular_area())
    }
}

fn main() {
    let rect = Rectangulo { base: 10.0, altura: 5.0 };
    let circ = Circulo { radio: 3.0 };

    // Uso de los métodos del trait Area
    println!("{}", rect.describir());
    println!("{}", circ.describir());
}

Explicación del Código

  1. trait Area: Define el contrato que cualquier forma geométrica debe seguir.
  2. fn calcular_area(&self) -> f64: Es una firma de método obligatoria dentro de Area. Los tipos deben proveer su propia lógica para retornar un f64.
  3. fn describir(&self) -> String: Es un método con implementación por defecto que utiliza self.calcular_area() para construir un mensaje.
  4. struct Rectangulo y struct Circulo: Son estructuras simples que actúan como los tipos de datos sobre los cuales implementaremos el comportamiento.
  5. impl Area for Rectangulo: Aquí se cumple el contrato para Rectangulo. Se implementa calcular_area usando los campos self.base y self.altura. Como no se define describir, se usa la lógica por defecto.
  6. impl Area for Circulo: Implementa calcular_area usando self.radio y la constante $\pi$. Aquí, el método describir se sobreescribe para cambiar el texto y aplicar un formato de dos decimales ({:.2}).
  7. main(): Instancia rect y circ, permitiendo llamar a describir() de manera uniforme, demostrando que ambos tipos comparten la interfaz definida por el trait Area.

44