rust-traits-comportamientos-interfaces

Traits en Rust: compartir comportamiento entre tipos

  • 4 min

Un trait es un contrato que define comportamientos que otros tipos pueden implementar.

Si vienes de Java o C#, conceptualmente se parece a una Interfaz (Interface). Si vienes de C++, es similar a una Clase Abstracta (con funciones virtuales puras). Si vienes de Haskell, es una Type Class.

En esencia, un Trait define una funcionalidad compartida. Le dice al compilador: “No me importa qué es este objeto, solo me importa que tenga un método llamado x.

Definir un trait

Imagina que estamos construyendo una aplicación de noticias. Tenemos Articulo, Tweet y EstadoDeFacebook. Queremos una forma unificada de pedir un “resumen” de cada uno de ellos.

Definimos un trait usando la palabra clave trait:

pub trait Resumible {
    // Declaramos la firma del método, pero no el cuerpo (normalmente)
    fn resumir(&self) -> String;
}
Copied!

Cualquier tipo que quiera ser Resumible debe implementar este método con esa firma exacta.

Implementar un trait en un tipo

Ahora vamos a implementar este comportamiento para dos structs distintos. La sintaxis es: impl NombreTrait for NombreTipo.

pub struct Articulo {
    pub titulo: String,
    pub autor: String,
    pub contenido: String,
}

impl Resumible for Articulo {
    fn resumir(&self) -> String {
        format!("{} por {}", self.titulo, self.autor)
    }
}

pub struct Tweet {
    pub usuario: String,
    pub contenido: String,
}

impl Resumible for Tweet {
    fn resumir(&self) -> String {
        format!("{}: {}", self.usuario, self.contenido)
    }
}
Copied!

Ahora, aunque Articulo y Tweet son tipos completamente diferentes, ambos comparten un comportamiento.

fn main() {
    let tweet = Tweet {
        usuario: String::from("rust_lang"),
        contenido: String::from("¡Los traits son geniales!"),
    };

    println!("Nuevo tweet: {}", tweet.resumir());
}
Copied!

Implementaciones por defecto

A diferencia de las interfaces antiguas de Java, los Traits en Rust pueden tener código. Podemos proporcionar un comportamiento predeterminado para que los tipos no tengan que implementarlo obligatoriamente si no quieren.

pub trait Resumible {
    // Implementación por defecto
    fn resumir(&self) -> String {
        String::from("(Leer más...)")
    }
}
Copied!

Si ahora creamos un struct y dejamos el bloque impl vacío, usará el defecto:

struct Foto {
    url: String,
}

impl Resumible for Foto {} // Bloque vacío = usar defecto

fn main() {
    let f = Foto { url: String::from("img.jpg") };
    println!("{}", f.resumir()); // Imprime: (Leer más...)
}
Copied!

Las implementaciones por defecto pueden llamar a otros métodos del mismo trait, incluso si esos otros métodos no tienen implementación por defecto. Esto permite crear “métodos plantilla”.

Traits como parámetros

Los traits se unen con las funciones para darnos polimorfismo. Podemos escribir una función que acepte cualquier cosa que implemente el trait Resumible.

// Sintaxis "impl Trait" (Azúcar sintáctico)
pub fn notificar(item: &impl Resumible) {
    println!("Noticia de última hora: {}", item.resumir());
}

fn main() {
    let articulo = Articulo { ... };
    let tweet = Tweet { ... };

    notificar(&articulo); // ✅ Funciona
    notificar(&tweet);    // ✅ Funciona
}
Copied!

La función notificar no sabe si le pasas un Tweet o un Artículo. Solo sabe que puede llamar a .resumir().

La regla del huérfano

Hay una restricción importante en Rust sobre dónde puedes implementar un trait. Para implementar un trait en un tipo, al menos uno de los dos debe ser local a tu Crate (tu proyecto).

  • ✅ Puedes implementar tu trait Resumible en tu tipo Tweet.
  • ✅ Puedes implementar tu trait Resumible en el tipo externo String (de la librería estándar).
  • ✅ Puedes implementar el trait externo Display (de std) en tu tipo Tweet.
  • NO puedes implementar el trait externo Display en el tipo externo String.
// ❌ Error: Impl de trait externo para tipo externo
impl std::fmt::Display for String { ... }
Copied!

Esta regla evita implementaciones incompatibles. Sin ella, dos librerías podrían definir de forma distinta cómo se imprime un String y no habría una elección coherente cuando ambas participaran en el mismo programa.