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.
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.
- Distinguir el
Tque viene del tipo (declarado enimpl<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
implcondicional cuyos métodos existen solo siTcumple un bound. - Reconocer ese patrón en tipos de la std como
Option,VecyPoint.
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
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>tienededupsolo siT: PartialEq, ysortsolo siT: Ord. UnVecde un tipo sin orden no ofrecesort.Option<T>tieneunwrap_or_defaultsolo siT: Default, ycopiedsolo siT: Copy.[T]tienecontainssolo siT: 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
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.
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.
- Añade a
Punto<T>un segundo bloqueimpl<T: Clone>con un métododuplicado(&self) -> (T, T)que clone ambas coordenadas. - Define
struct Opaco;sin derivar nada, construyePunto::nuevo(Opaco, Opaco)e intenta llamar amostrar_mayor: lee el error e identifica qué bound falta. - Escribe un método genérico
fn con<U>(self, u: U) -> Punto<U>que descarteTy devuelva unPuntodel nuevo tipoU. - Comprueba en la práctica que
vec![3, 1, 2].sort()funciona pero que unVecde un tipo sinOrdno ofrecesort; explica por qué usando la idea de impl condicional. - Investiga el impl general
impl<T: Display> ToString for Ty explica cómo un solo bloque condicional dota deto_stringa infinitos tipos a la vez.