Ir al contenido

Genéricos en Rust

Ya has usado genéricos, aunque quizá no los llamaras así: Vec<i32> y Option<T> los usan. En esta página aprenderás a escribir tus propias funciones y structs genéricas.

Imagina que quieres una función que devuelva el número más grande de una lista, tanto para i32 como para f64. Sin genéricos, tendrías que escribirla dos veces:

fn largest_i32(numbers: &[i32]) -> i32 {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}
fn largest_f64(numbers: &[f64]) -> f64 {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}

Es la misma lógica, copiada y pegada para cada tipo. Con genéricos, la escribes una sola vez.

fn largest<T: PartialOrd + Copy>(numbers: &[T]) -> T {
let mut largest = numbers[0];
for &n in numbers {
if n > largest {
largest = n;
}
}
largest
}
fn main() {
let integers = [10, 25, 3, 47, 8];
let floats = [1.5, 2.8, 0.3];
println!("{}", largest(&integers));
println!("{}", largest(&floats));
}

Salida:

47
2.8
▶ Pruébalo tú mismo

<T: PartialOrd + Copy> se lee así: “para cualquier tipo T, siempre que se pueda comparar (PartialOrd) y copiar (Copy)”. T es un marcador de posición: Rust genera una versión de la función para el tipo concreto con el que la llames de verdad (i32, f64, etc.).

Las structs también pueden ser genéricas:

struct Pair<T> {
first: T,
second: T,
}
fn main() {
let numbers = Pair { first: 5, second: 10 };
let words = Pair { first: "hola", second: "mundo" };
println!("{} {}", numbers.first, numbers.second);
println!("{} {}", words.first, words.second);
}

Salida:

5 10
hola mundo
▶ Pruébalo tú mismo

Una sola definición de Pair<T> sirve para números, texto o cualquier otro tipo: numbers es un Pair<i32> y words es un Pair<&str>, pero la struct solo la escribiste una vez.

No. Rust genera una versión aparte, especializada, de la función o la struct para cada tipo concreto que uses de verdad (a esto se le llama monomorfización). Cuando tu programa se ejecuta, ya no queda nada de código genérico: solo funciones normales con sus tipos concretos, igual de rápidas que si hubieras escrito cada una a mano.

Puedes añadir métodos a una struct genérica con un bloque impl, igual que en la página de Structs. Lo único nuevo es que el impl también necesita su propio <T>:

struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
fn swap(self) -> Pair<T> {
Pair { first: self.second, second: self.first }
}
}
impl Pair<f64> {
fn average(&self) -> f64 {
(self.first + self.second) / 2.0
}
}
fn main() {
let words = Pair::new("izquierda", "derecha").swap();
println!("{} {}", words.first, words.second);
let scores = Pair::new(7.0, 9.0);
println!("{}", scores.average());
}

Salida:

derecha izquierda
8
▶ Pruébalo tú mismo

Aquí hay dos tipos de bloque impl:

  • impl<T> Pair<T> significa “estos métodos existen para un Pair de cualquier tipo”. Lee el primer <T> como “para cualquier tipo T”. Sin él, Rust pensaría que T es el nombre de un tipo real y se quejaría de que no lo encuentra.
  • impl Pair<f64> no lleva <T>. Sus métodos existen solo para Pair<f64>. Por eso average puede usar + y / 2.0. Prueba a llamar a words.average() y el compilador te dirá que ese método no existe para Pair<&str>.

Una struct también puede tener más de un parámetro de tipo, como struct Pair<T, U> { first: T, second: U }, cuando los dos campos pueden ser de tipos distintos. Y cuando los trait bounds se hacen largos, puedes moverlos a una cláusula where para que la firma se lea mejor. Eso se explica en la página de traits avanzados.

Escribe fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T, que devuelva el elemento más pequeño. Pruébala con [8, 3, 5] y con los caracteres ['r', 'u', 's', 't']. Pista: es la función largest de esta página con una comparación al revés.

Ver solución
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut smallest = items[0];
for &item in items {
if item < smallest {
smallest = item;
}
}
smallest
}
fn main() {
println!("{}", smallest(&[8, 3, 5]));
println!("{}", smallest(&['r', 'u', 's', 't']));
}

Salida:

3
r
▶ Pruébalo tú mismo

Añade a Pair<T> un método larger(&self) -> &T que devuelva una referencia al campo que sea mayor. Pruébalo con un Pair de números y con un Pair de palabras. Pista: comparar necesita PartialOrd, así que pon el bound en el propio impl: impl<T: PartialOrd> Pair<T>.

Ver solución
struct Pair<T> {
first: T,
second: T,
}
impl<T: PartialOrd> Pair<T> {
fn larger(&self) -> &T {
if self.first > self.second {
&self.first
} else {
&self.second
}
}
}
fn main() {
let numbers = Pair { first: 3, second: 9 };
let words = Pair { first: "pera", second: "manzana" };
println!("{}", numbers.larger());
println!("{}", words.larger());
}

Salida:

9
pera
▶ Pruébalo tú mismo

Las palabras se comparan por orden alfabético, así que "pera" es mayor que "manzana" porque la p va después de la m.

  • Los genéricos te permiten escribir una sola función o struct que funciona con varios tipos, en lugar de duplicar código.
  • <T> es un tipo de marcador de posición; los trait bounds (T: PartialOrd) limitan qué puede ser T.
  • impl<T> Pair<T> añade métodos para cualquier T; impl Pair<f64> añade métodos solo para un tipo concreto.
  • El código genérico se compila en versiones especializadas para cada tipo, así que no tiene coste al ejecutar.

Comprueba lo aprendido

1. ¿Qué problema resuelven los genéricos?

2. En fn largest<T: PartialOrd + Copy>(...), ¿qué hace PartialOrd + Copy?

3. ¿Usar genéricos cuesta algo al ejecutar, comparado con código escrito a mano?

4. ¿Qué te permite hacer una sola definición de struct Pair<T>?

Puntuación: 0 / 4