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.
El problema que resuelven los genéricos
Sección titulada «El problema que resuelven los genéricos»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.
Una función genérica
Sección titulada «Una función genérica»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:
472.8<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.).
Una struct genérica
Sección titulada «Una struct genérica»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 10hola mundoUna 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.
¿Esto cuesta algo al ejecutar?
Sección titulada «¿Esto cuesta algo al ejecutar?»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.
Métodos en una struct genérica
Sección titulada «Métodos en una struct genérica»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 izquierda8Aquí hay dos tipos de bloque impl:
impl<T> Pair<T>significa “estos métodos existen para unPairde cualquier tipo”. Lee el primer<T>como “para cualquier tipoT”. Sin él, Rust pensaría queTes 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 paraPair<f64>. Por esoaveragepuede usar+y/ 2.0. Prueba a llamar awords.average()y el compilador te dirá que ese método no existe paraPair<&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.
Práctica
Sección titulada «Práctica»1. Un smallest genérico
Sección titulada «1. Un smallest genérico»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:
3r2. La mitad más grande de un Pair
Sección titulada «2. La mitad más grande de un Pair»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:
9peraLas palabras se comparan por orden alfabético, así que "pera" es mayor que "manzana" porque la p va después de la m.
Resumen
Sección titulada «Resumen»- 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 serT.impl<T> Pair<T>añade métodos para cualquierT;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 que aprendiste
Sección titulada «Comprueba lo que aprendiste»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
Para leer más
Sección titulada «Para leer más»- Generic Data Types — The Rust Book (en inglés): genéricos en funciones, structs, enums y métodos, incluido el ejemplo de
largest. - Generics — Rust by Example (en inglés): ejemplos cortos y ejecutables de funciones y structs genéricas, y de su sintaxis.
- Implementation — Rust by Example (en inglés): más sobre los bloques
impl<T>y sobre implementar métodos para un solo tipo concreto. - Bounds — Rust by Example (en inglés): cómo los trait bounds limitan lo que puede ser un tipo genérico, y por qué los necesitas.
- Generic parameters — The Rust Reference (en inglés): las reglas exactas, para cuando quieras todos los detalles.