wandres.dev
TRAITS · comportamiento compartido

Implementaciones por defecto: métodos requeridos y provistos

Un trait puede dar cuerpo a sus propios métodos. Cómo una implementación por defecto ofrece comportamiento gratis a todos los tipos, cómo un tipo puede sobreescribirla, y por qué una superficie mínima requerida sostiene una biblioteca entera de métodos derivados.

⏱ 17 min

Un trait no tiene por qué limitarse a pedir. También puede dar. Cuando una firma del trait trae su propio cuerpo, se convierte en una implementación por defecto: comportamiento que todo tipo que firme el contrato recibe gratis, sin escribir una línea. El tipo puede aceptarlo tal cual o sobreescribirlo. Esta capa de valores por defecto, construida sobre un núcleo mínimo de métodos requeridos, es el mecanismo que hace que un trait como Iterator te regale decenas de métodos a cambio de implementar uno solo.

🎯 Al terminar esta lección sabrás
  • Distinguir métodos requeridos (sin cuerpo) de provistos (con cuerpo por defecto).
  • Escribir una implementación por defecto que apoye en los métodos requeridos.
  • Sobreescribir un método por defecto en un impl concreto.
  • Reconocer el patrón “núcleo mínimo + biblioteca derivada” en la práctica.

Un método con cuerpo dentro del trait

Hasta ahora las firmas del trait terminaban en punto y coma. Si en su lugar les das un bloque, defines su comportamiento por defecto:

trait Saludo {
    fn nombre(&self) -> String;          // requerido: sin cuerpo

    fn saludar(&self) -> String {         // provisto: con cuerpo por defecto
        format!("Hola, {}", self.nombre())
    }
}

nombre es un método requerido: el trait lo declara pero no lo define, así que cada tipo debe aportarlo. saludar es un método provisto: el trait ya trae su cuerpo, y ese cuerpo se apoya en nombre. La consecuencia es notable: un tipo que implemente el trait solo tiene que dar nombre, y recibe saludar de regalo.

struct Persona { nombre: String }

impl Saludo for Persona {
    fn nombre(&self) -> String {
        self.nombre.clone()
    }
    // no implementamos saludar: heredamos el cuerpo por defecto
}

fn main() {
    let p = Persona { nombre: String::from("Ada") };
    println!("{}", p.saludar());   // Hola, Ada
}

El impl está casi vacío y aun así p.saludar() funciona. El trait escribió el algoritmo una vez; el tipo solo rellenó la pieza que el algoritmo no podía adivinar.

Sobreescribir el valor por defecto

Un método provisto es un punto de partida, no una sentencia. Cualquier impl puede reemplazarlo dando su propio cuerpo, sin ninguna palabra clave especial —nada de override—: basta con volver a declararlo.

struct Robot { serie: u32 }

impl Saludo for Robot {
    fn nombre(&self) -> String {
        format!("unidad-{}", self.serie)
    }

    fn saludar(&self) -> String {         // sobreescribe el default
        format!("BEEP. Identificador {}.", self.nombre())
    }
}

fn main() {
    let p = Persona { nombre: String::from("Ada") };
    let r = Robot { serie: 7 };
    println!("{}", p.saludar());   // Hola, Ada  (cuerpo por defecto)
    println!("{}", r.saludar());   // BEEP. Identificador unidad-7.  (sobreescrito)
}

Robot cambia el saludo; Persona conserva el genérico. Ambos cumplen el mismo contrato con comportamientos distintos. Sobreescribir un método provisto no afecta a los demás: si el trait ofreciera también despedir por defecto, Robot seguiría heredándolo aunque haya redefinido saludar.

⚠️
No hay super: el default sobreescrito desaparece

A diferencia de la herencia de clases, aquí no existe un super.saludar() con el que llamar a la versión por defecto desde la sobreescritura. Una vez que un impl proporciona su propio saludar, el cuerpo por defecto queda inaccesible para ese tipo. Si necesitas reutilizar parte de la lógica común, extráela a una función libre o a otro método requerido y haz que ambos —el default y las sobreescrituras— la invoquen. Los traits favorecen la composición explícita sobre las cadenas implícitas de llamadas a la clase padre.

flowchart TB
req[metodo requerido nombre sin cuerpo] --> d1[metodo provisto saludar apoya en nombre]
req --> d2[metodo provisto despedir apoya en nombre]
d1 --> tipo[un tipo solo implementa nombre]
d2 --> tipo
tipo --> gratis[recibe saludar y despedir gratis]
tipo --> over[y puede sobreescribir cualquiera de los dos]

El patrón: núcleo mínimo, biblioteca derivada

La verdadera potencia aparece cuando muchos métodos provistos se apoyan en un puñado de métodos requeridos. El trait autor escribe una capa de comportamiento derivado que depende solo de las operaciones abstractas del núcleo; el implementador aporta ese núcleo mínimo y hereda toda la capa.

trait Coleccion {
    fn longitud(&self) -> usize;          // único método requerido

    fn esta_vacia(&self) -> bool {        // derivado
        self.longitud() == 0
    }
    fn describir(&self) -> String {        // derivado
        if self.esta_vacia() {
            String::from("vacia")
        } else {
            format!("{} elementos", self.longitud())
        }
    }
}

Implementa longitud y obtienes esta_vacia y describir sin esfuerzo. Fíjate en que un método provisto puede apoyarse en otro provisto (describir llama a esta_vacia): el trait compone su propia lógica sobre el mismo núcleo abstracto.

struct Bolsa { articulos: Vec<String> }

impl Coleccion for Bolsa {
    fn longitud(&self) -> usize { self.articulos.len() }
    // esta_vacia y describir llegan gratis
}

fn main() {
    let b = Bolsa { articulos: vec![String::from("pan"), String::from("sal")] };
    println!("{}", b.describir());   // 2 elementos
    println!("{}", b.esta_vacia());  // false
}

Todo el comportamiento observable de Bolsa como colección se deduce de una sola línea de implementación. El resto lo escribió el trait, una vez, para cualquier tipo presente o futuro.

ℹ️
No todo método provisto depende de los requeridos

Algunos métodos por defecto se apoyan en los requeridos —como saludar sobre nombre—, pero otros son autosuficientes y no tocan el núcleo. Lo que los define no es de qué dependen, sino que el trait ya trae su cuerpo. La biblioteca estándar mezcla ambos estilos: Iterator::size_hint trae un default genérico que no consulta next, mientras que count sí lo agota elemento a elemento. Al diseñar, distingue lo que debe variar por tipo (requerido) de lo que puede fijarse una vez para todos (provisto).

El esqueleto algorítmico: cómo un método requerido paga decenas

Este patrón no es una comodidad menor; es una inversión del coste de la abstracción, y su expresión canónica es el trait Iterator de la biblioteca estándar. Iterator tiene un solo método requerido, next, que devuelve el siguiente elemento o None. Sobre esa única operación, la biblioteca define más de setenta métodos provistos: map, filter, fold, sum, collect, zip, take, enumerate… Cuando implementas Iterator para tu propio tipo, escribes next —a veces tres líneas— y recibes gratis toda esa maquinaria de combinadores, ya optimizada y probada. Lo que ocurre en el fondo tiene nombre en el diseño de software: es el patrón método plantilla (template method) elevado de un idiom de la orientación a objetos a un mecanismo del lenguaje. La capa por defecto es un esqueleto algorítmico escrito en términos de operaciones abstractas —solo next—, de modo que cada iterador concreto lo “rellena” con su forma particular de avanzar y el esqueleto se especializa para él en compilación, sin coste. La lección de diseño es profunda y simétrica: cuando definas un trait, invierte el esfuerzo en identificar el conjunto mínimo de métodos verdaderamente primitivos —los que no pueden derivarse de otros— y decláralos requeridos; todo lo demás constrúyelo como métodos provistos sobre ese núcleo. El implementador escribe lo mínimo, tú escribes el apalancamiento una sola vez, y la corrección de la capa derivada se garantiza para todos los tipos presentes y futuros. Un buen trait no se mide por cuántos métodos pide, sino por cuán pocos pide para cuántos entrega.

📝
Lo esencial

Un método del trait con cuerpo es una implementación por defecto: el tipo la recibe gratis y solo necesita aportar los métodos requeridos. Puede sobreescribirla volviendo a declararla, sin override y sin super. Los métodos provistos pueden apoyarse en los requeridos —e incluso entre sí—, de modo que un núcleo mínimo sostiene una biblioteca entera de comportamiento derivado. Es el patrón que hace que implementar next te regale todo Iterator.

⚔️ Diseña un núcleo mínimo
  1. Escribe un trait Sonido con un método requerido frecuencia(&self) -> f64 y un método provisto es_agudo(&self) -> bool que devuelva true si la frecuencia supera 2000.
  2. Implementa Sonido para dos structs distintos aportando solo frecuencia; comprueba que ambos obtienen es_agudo.
  3. Sobreescribe es_agudo en uno de ellos con otro umbral y verifica que el otro conserva el default.
  4. Añade un método provisto describir que use es_agudo y observa cómo un default se apoya en otro default.
  5. Consulta la firma de Iterator en la documentación estándar y cuenta cuántos métodos provistos cuelgan del único next requerido.