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;
}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)
}
}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());
}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...)")
}
}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...)
}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
}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
Resumibleen tu tipoTweet. - ✅ Puedes implementar tu trait
Resumibleen el tipo externoString(de la librería estándar). - ✅ Puedes implementar el trait externo
Display(de std) en tu tipoTweet. - ❌ NO puedes implementar el trait externo
Displayen el tipo externoString.
// ❌ Error: Impl de trait externo para tipo externo
impl std::fmt::Display for String { ... }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.