Generics in Rust
I generics in Rust permettono di scrivere codice che funziona con diversi tipi senza duplicazione. Grazie alla monomorfizzazione, i generics hanno zero costi a runtime.
Funzioni Generiche
fn massimo<T: PartialOrd>(a: T, b: T) -> T {
if a >= b { a } else { b }
}
fn main() {
println!("{}", massimo(10, 20)); // 20
println!("{}", massimo(3.14, 2.71)); // 3.14
println!("{}", massimo("abc", "xyz")); // xyz
}
Struct Generiche
#[derive(Debug)]
struct Punto<T> {
x: T,
y: T,
}
// Struct con tipi diversi
#[derive(Debug)]
struct Coppia<A, B> {
primo: A,
secondo: B,
}
fn main() {
let intero = Punto { x: 5, y: 10 };
let decimale = Punto { x: 1.5, y: 2.5 };
let misto = Coppia { primo: "nome", secondo: 42 };
}
Impl con Generics
impl<T> Punto<T> {
fn new(x: T, y: T) -> Self {
Punto { x, y }
}
fn x(&self) -> &T {
&self.x
}
}
// Implementazione solo per un tipo specifico
impl Punto<f64> {
fn distanza_da_origine(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Enum Generici
Option<T> e Result<T, E> sono gli esempi più comuni:
enum Opzione<T> {
Qualcosa(T),
Niente,
}
enum Risultato<T, E> {
Ok(T),
Err(E),
}
Trait Bounds
Limitano i tipi accettati da un generico:
use std::fmt::Display;
// Sintassi con :
fn stampa<T: Display>(valore: T) {
println!("{}", valore);
}
// Sintassi con where (più leggibile per bounds complessi)
fn elabora<T, U>(t: T, u: U) -> String
where
T: Display + Clone,
U: Display + Debug,
{
format!("{} - {:?}", t, u)
}
// Sintassi impl Trait (per parametri)
fn stampa_display(valore: impl Display) {
println!("{}", valore);
}
Bounds Multipli
use std::fmt::{Display, Debug};
fn confronta_e_stampa<T: PartialOrd + Display>(a: T, b: T) {
if a > b {
println!("{} è maggiore di {}", a, b);
} else {
println!("{} è minore o uguale a {}", a, b);
}
}
Const Generics
Permettono di parametrizzare sui valori costanti:
fn stampa_array<T: Debug, const N: usize>(arr: [T; N]) {
println!("{:?}", arr);
}
struct Buffer<const N: usize> {
dati: [u8; N],
}
fn main() {
stampa_array([1, 2, 3]); // N = 3
stampa_array([1, 2, 3, 4, 5]); // N = 5
let buf = Buffer::<1024> { dati: [0; 1024] };
}
Monomorfizzazione
Il compilatore genera codice specifico per ogni tipo concreto usato:
fn doppio<T: std::ops::Mul<Output = T> + Copy>(x: T) -> T {
x * x
}
// Il compilatore genera:
// fn doppio_i32(x: i32) -> i32 { x * x }
// fn doppio_f64(x: f64) -> f64 { x * x }
fn main() {
doppio(5_i32); // Usa la versione i32
doppio(3.14); // Usa la versione f64
}
Questo garantisce zero overhead rispetto al codice scritto manualmente per ogni tipo.
Conclusione
I generics sono uno dei pilastri di Rust, permettendo di scrivere codice riutilizzabile e type-safe senza sacrificare le performance. I trait bounds garantiscono che i tipi generici soddisfino i requisiti necessari, e la monomorfizzazione elimina qualsiasi costo a runtime. Usa where per bounds complessi e i const generics per parametrizzare sulla dimensione.