Traits y Genéricos
Los traits describen comportamiento compartido entre tipos distintos, y los genéricos permiten que una sola función funcione con todos ellos sin sacrificar velocidad.
Un trait es la forma que tiene Rust de describir un comportamiento que distintos tipos pueden compartir, sin que esos tipos necesiten estar relacionados por herencia. Si has usado interfaces en Java o TypeScript, o protocolos en Swift, la idea te resultará familiar: un trait declara un conjunto de firmas de métodos, y cualquier tipo puede sumarse implementándolas. Lo que hace que los traits de Rust sean más que un capricho sintáctico es que el compilador los usa por todas partes: la sobrecarga de operadores, el formateo con {} y {:?}, la iteración, la igualdad, e incluso la seguridad entre hilos son solo traits (Add, Display, Debug, Iterator, PartialEq, Send) que los tipos comunes implementan. Una vez que sabes definir e implementar tus propios traits, estás usando el mismo mecanismo sobre el que corre la biblioteca estándar, no una función aparte para principiantes.
Definir un trait se parece a declarar una interfaz: trait Summary { fn summarize(&self) -> String; } enumera la firma de un método sin cuerpo. Un tipo se suma con impl Summary for Article { fn summarize(&self) -> String { ... } }, y debe proveer cada método que el trait declare y que no tenga ya una implementación por defecto — el compilador se niega a compilar un tipo que implemente el trait solo a medias. Fíjate en que el trait y la implementación son dos bloques separados: esa separación es lo que te permite implementar traits de la biblioteca estándar (como Display) para tus propios tipos y —sujeto a la regla del huérfano ('orphan rule') de Rust— implementar tus propios traits para tipos que no escribiste tú, como Vec<T>.
Los traits también pueden llevar cuerpos de método por defecto, y ahí es donde se ganan el sueldo frente a una interfaz común. Si summarize tiene una implementación de respaldo razonable escrita en términos de otro método que el trait exige (como author), cada tipo que implemente el trait recibe ese comportamiento gratis y solo necesita sobrescribirlo cuando de verdad quiere algo distinto. Esto invierte el boilerplate habitual: en lugar de que cada tipo reimplemente la lógica compartida, solo los tipos que necesitan un comportamiento a medida escriben algo de código.
Los genéricos son donde los traits empiezan a hacer trabajo real. Una función como fn largest<T: PartialOrd>(list: &[T]) -> &T es genérica sobre cualquier tipo T, pero restringe a T con un límite de trait ('trait bound'): T debe implementar PartialOrd, porque el cuerpo de la función compara elementos con >. Sin ese límite, el compilador tendría que aceptar un T que quizás no admita comparación alguna, y se niega a arriesgarse: verifica el límite en el sitio de definición, no en cada sitio de llamada. Esto también explica por qué los genéricos de Rust no cuestan nada en tiempo de ejecución: el compilador genera una versión separada y totalmente concreta de largest para cada tipo con el que realmente la llames (un proceso llamado monomorfización), así que el código compilado es exactamente tan rápido como si hubieras escrito a mano largest_i32 y largest_char por separado.
Cuando una función solo necesita aceptar 'algo que implemente un trait' en lugar de trabajar con un genérico explícito, impl Trait es el atajo exacto para eso. fn notify(item: &impl Summary) se lee de forma natural y equivale a escribir fn notify<T: Summary>(item: &T) — el compilador la sigue monomorfizando, así que es puro azúcar sintáctico, no un mecanismo de despacho distinto. impl Trait también funciona en la posición de retorno — fn create_tweet() -> impl Summary permite que una función devuelva un tipo concreto sin revelar cuál es, algo valiosísimo cuando ese tipo no tiene nombre, como el tipo de un closure o el de una cadena de adaptadores de iterador.
Sin embargo, hay una diferencia real en la posición de retorno: impl Trait sigue comprometiendo a la función a devolver exactamente un tipo concreto, elegido en tiempo de compilación — no puedes hacer que una rama devuelva un Tweet y otra un Article desde la misma función que retorna impl Summary. Si de verdad necesitas devolver tipos concretos distintos detrás de una misma interfaz, o guardar una mezcla de tipos en la misma colección, necesitas despacho dinámico.
Para eso existe dyn Trait. Un &dyn Summary o un Box<dyn Summary> borra el tipo concreto detrás de una vtable —una tabla de punteros a función construida en tiempo de ejecución—, de modo que un Vec<Box<dyn Summary>> puede contener de verdad un Tweet y un Article lado a lado, y cada llamada a .summarize() se resuelve en tiempo de ejecución en lugar de compilarse como una llamada directa. Esa flexibilidad no es gratis: cuesta una llamada indirecta y una reserva en el heap (vía Box) que el despacho estático evita por completo. Por ahora, piensa en dyn Trait como la válvula de escape para colecciones heterogéneas, y en la elección como 'estático por defecto, dinámico cuando lo necesites' — las próximas lecciones sobre smart pointers lo usarán todo el tiempo.
trait Summary {fn author(&self) -> String;// A default method: any type gets this for free unless it overrides it.fn summarize(&self) -> String {format!("(Read more from {}...)", self.author())}}struct Article {headline: String,author_name: String,}impl Summary for Article {fn author(&self) -> String {self.author_name.clone()}// Article overrides the default because it wants a different format.fn summarize(&self) -> String {format!("{}, by {}", self.headline, self.author_name)}}struct Tweet {username: String,}impl Summary for Tweet {fn author(&self) -> String {format!("@{}", self.username)}// No override here: Tweet relies on the default summarize().}fn main() {let article = Article {headline: String::from("Rust 2.0 Announced"),author_name: String::from("Jane Doe"),};let tweet = Tweet {username: String::from("ferris"),};println!("{}", article.summarize());println!("{}", tweet.summarize());}
Defining a trait with one required method and one default method, then implementing it for two different types — Tweet gets the default summarize() for free, Article overrides it.
fn largest<T: PartialOrd>(list: &[T]) -> &T {let mut largest = &list[0];for item in list {if item > largest {largest = item;}}largest}fn main() {let numbers = vec![34, 50, 25, 100, 65];let result = largest(&numbers);println!("The largest number is {}", result);let chars = vec!['y', 'm', 'a', 'q'];let result = largest(&chars);println!("The largest char is {}", result);}
A generic function bounded by PartialOrd: the bound is required because the function body compares elements with >, and the compiler generates a separate concrete version for each type it's called with.
trait Summary {fn summarize(&self) -> String;}struct Tweet {username: String,content: String,}impl Summary for Tweet {fn summarize(&self) -> String {format!("@{}: {}", self.username, self.content)}}// `impl Trait` in argument position: sugar for a generic bound.fn notify(item: &impl Summary) {println!("Breaking news! {}", item.summarize());}// `impl Trait` in return position: callers get a concrete type back// without needing to know which one it is.fn create_tweet() -> impl Summary {Tweet {username: String::from("rustlang"),content: String::from("Rust 1.80 is out!"),}}// `dyn Trait` behind a reference: works uniformly with several concrete// types through one pointer, resolved at runtime instead of compile time.fn notify_dynamic(item: &dyn Summary) {println!("Breaking news! {}", item.summarize());}fn main() {let tweet = create_tweet();notify(&tweet);notify_dynamic(&tweet);}
impl Trait sugar for accepting or returning 'some type that implements Summary' with static dispatch, contrasted with dyn Trait, which erases the concrete type so callers can be resolved at runtime instead.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. Why does fn largest<T: PartialOrd>(list: &[T]) -> &T need the trait bound PartialOrd, instead of compiling for any T?