wandres.dev
GENÉRICOS · código para cualquier tipo

Métodos genéricos e impl condicionales: la API que el tipo se gana

Un método puede tener su propio parámetro de tipo, y un bloque `impl` puede exigir bounds que hagan aparecer métodos solo cuando `T` los cumple. Los impl condicionales como `impl<T: Display>` y el patrón que la biblioteca estándar usa por todas partes.

⏱ 17 min

Ya sabes parametrizar un struct y darle métodos con impl<T>. Ahora llega la vuelta de tuerca que separa a quien usa genéricos de quien los domina: un método puede introducir su propio parámetro de tipo, distinto del struct, y —más potente aún— un bloque impl puede llevar un bound que haga que sus métodos existan solo cuando el tipo del contenedor cumple ciertas capacidades. Un Punto<T> cualquiera tendrá unos métodos; un Punto<T> cuyo T sabe imprimirse tendrá esos y algunos más. La API deja de ser fija: se la gana el tipo.

🎯 Al terminar esta lección sabrás
  • Distinguir el T que viene del tipo (declarado en impl<T>) del parámetro propio de un método.
  • Escribir un método con su propio parámetro de tipo, independiente del struct.
  • Escribir un impl condicional cuyos métodos existen solo si T cumple un bound.
  • Reconocer ese patrón en tipos de la std como Option, Vec y Point.

El T del tipo frente al T del método

Cuando escribes impl<T> Envoltura<T>, ese T viene fijado por la instancia: en una Envoltura<i32>, T ya es i32 en todos los métodos. Pero un método puede declarar su propio parámetro de tipo, elegido de nuevo en cada llamada, ortogonal al del struct:

struct Envoltura<T> {
    valor: T,
}

impl<T> Envoltura<T> {
    // U es del método, no del struct: se decide en cada llamada
    fn combinar<U>(self, otro: U) -> (T, U) {
        (self.valor, otro)
    }
}

Aquí T está atado a la envoltura (self.valor es T), pero U es libre: puedes combinar una misma Envoltura<i32> con un &str en una llamada y con un bool en la siguiente. Son dos ejes de genericidad independientes conviviendo en la misma firma.

let e = Envoltura { valor: 10 };
let (a, b) = e.combinar("diez");   // T = i32, U = &str
assert_eq!(a, 10);
assert_eq!(b, "diez");

Contrasta con un método que se apoya en el T del struct: obtener no declara ningún parámetro nuevo, solo devuelve lo que ya hay dentro.

impl<T> Envoltura<T> {
    fn obtener(&self) -> &T {
        &self.valor
    }
}

Dos métodos, dos orígenes de genericidad: obtener reutiliza el T de la instancia; combinar añade su propio U. Distinguirlos es la clave para leer sin perderte las firmas más densas de la biblioteca.

impl condicional: métodos que dependen de T

Un mismo tipo puede tener varios bloques impl, y cada uno puede pedir bounds distintos. Los métodos de un bloque con bound solo existen para las instancias cuyo T cumple ese bound. Es el patrón más característico del sistema:

use std::fmt::Display;

struct Punto<T> {
    x: T,
    y: T,
}

// Bloque incondicional: TODO Punto<T> tiene `nuevo`
impl<T> Punto<T> {
    fn nuevo(x: T, y: T) -> Self {
        Punto { x, y }
    }
}

// Bloque condicional: SOLO si T sabe imprimirse y compararse
impl<T: Display + PartialOrd> Punto<T> {
    fn mostrar_mayor(&self) {
        if self.x >= self.y {
            println!("la mayor coordenada es x = {}", self.x);
        } else {
            println!("la mayor coordenada es y = {}", self.y);
        }
    }
}

El efecto es quirúrgico. Punto<i32> tiene nuevo y mostrar_mayor, porque i32 cumple Display + PartialOrd. Pero si defines un struct Opaco que no implementa esos traits, un Punto<Opaco> tendrá nuevo y no tendrá mostrar_mayor: el método sencillamente no existe para esa instanciación, y llamarlo es un error de compilación, no de ejecución.

Punto::nuevo(3, 7).mostrar_mayor();   // OK: i32 cumple los bounds

struct Opaco;
let p = Punto::nuevo(Opaco, Opaco);   // OK: `nuevo` no exige nada
// p.mostrar_mayor();                 // ERROR: Opaco no implementa Display
ℹ️
El conjunto de métodos no es fijo: se calcula por instancia

No pienses en “los métodos de Punto” como una lista cerrada. Piensa en ellos como una función del parámetro: para cada T concreto, el compilador reúne todos los bloques impl cuyos bounds cumple T y suma sus métodos. Punto<i32> y Punto<Opaco> son el mismo tipo genérico, pero exponen APIs distintas. Es polimorfismo estructural resuelto en compilación: el tipo obtiene exactamente las capacidades que se ha ganado.

El patrón en la biblioteca estándar

Una vez que ves este patrón, lo reconoces en toda la std. Es la razón de que muchos métodos “aparezcan y desaparezcan” según el tipo que metas en un contenedor:

  • Vec<T> tiene dedup solo si T: PartialEq, y sort solo si T: Ord. Un Vec de un tipo sin orden no ofrece sort.
  • Option<T> tiene unwrap_or_default solo si T: Default, y copied solo si T: Copy.
  • [T] tiene contains solo si T: PartialEq.

El caso extremo es el impl general (blanket impl): la std implementa ToString para todo T que cumpla Display. Por eso cualquier tipo que sepa mostrarse obtiene gratis un método to_string, sin escribir una línea. Es un impl<T: Display> ToString for T: un solo bloque condicional que dota de una capacidad a una familia entera de tipos de golpe.

// En la std, conceptualmente:
// impl<T: Display> ToString for T { ... }

let s: String = 42.to_string();        // i32: Display  ⇒  tiene to_string
let t: String = 3.14.to_string();      // f64: Display  ⇒  también

Y lo ves en acción con Vec: el método sort aparece solo si el elemento es Ord.

let mut nums = vec![3, 1, 2];
nums.sort();                       // i32: Ord  ⇒  sort disponible
assert_eq!(nums, vec![1, 2, 3]);

struct SinOrden(i32);
let mut raros = vec![SinOrden(3), SinOrden(1)];
// raros.sort();                   // ERROR: SinOrden no implementa Ord
flowchart TB
base[impl de T sobre Punto de T] --> todos[Todo Punto tiene nuevo]
cond[impl de T con Display y PartialOrd] --> algunos[Solo esos Punto tienen mostrar_mayor]
i32[Punto de i32 cumple los bounds] --> ambos[Obtiene nuevo y mostrar_mayor]
opaco[Punto de Opaco no cumple] --> solo[Obtiene solo nuevo]
style todos fill:#89b4fa,color:#11111b
style ambos fill:#a6e3a1,color:#11111b
style solo fill:#f9e2af,color:#11111b
El tipo se gana su API según lo que sabe hacer

Los impl condicionales son la respuesta de Rust a una pregunta que la herencia contesta mal. En un lenguaje de clases, para que un tipo tenga un método debe heredarlo de una jerarquía decidida por adelantado, y a menudo carga con métodos que no le convienen (el clásico problema de la clase base frágil). Rust invierte la lógica: un tipo no tiene un método por quién es, sino por qué sabe hacer. mostrar_mayor existe para Punto<T> únicamente cuando T puede imprimirse y compararse, porque es exactamente lo que el método necesita, ni más ni menos. Esto produce APIs que se adaptan al tipo con precisión de bisturí: Vec<i32> puede ordenarse, Vec<MiTipoSinOrden> no, y esa diferencia está garantizada en compilación, sin coste ni comprobación en ejecución. La consecuencia es doble. Como autor de un genérico, expones cada capacidad justo bajo el bound que la hace correcta, y el compilador se encarga de que nadie la use sin cumplirlo. Como usuario, el autocompletado te muestra solo los métodos que tu tipo se ha ganado; los que no aparecen es porque tu tipo, honestamente, no puede darlos. Es la diferencia entre preguntar “¿de qué clase desciende este objeto?” y preguntar “¿qué es capaz de hacer este tipo?”. Rust hace la segunda pregunta, y los impl condicionales son su respuesta, verificada y gratuita.

📝
Lo esencial de los impl condicionales

Un tipo puede tener varios bloques impl, cada uno con sus bounds. Los métodos de un bloque existen solo para las instanciaciones cuyo T cumple ese bound, decidido en compilación y sin coste en ejecución. Por eso Punto<i32> y Punto<Opaco> exponen APIs distintas pese a compartir definición. Y un método puede además introducir su propio parámetro de tipo, independiente del struct.

⚔️ Haz que la API dependa del tipo
  1. Añade a Punto<T> un segundo bloque impl<T: Clone> con un método duplicado(&self) -> (T, T) que clone ambas coordenadas.
  2. Define struct Opaco; sin derivar nada, construye Punto::nuevo(Opaco, Opaco) e intenta llamar a mostrar_mayor: lee el error e identifica qué bound falta.
  3. Escribe un método genérico fn con<U>(self, u: U) -> Punto<U> que descarte T y devuelva un Punto del nuevo tipo U.
  4. Comprueba en la práctica que vec![3, 1, 2].sort() funciona pero que un Vec de un tipo sin Ord no ofrece sort; explica por qué usando la idea de impl condicional.
  5. Investiga el impl general impl<T: Display> ToString for T y explica cómo un solo bloque condicional dota de to_string a infinitos tipos a la vez.