I tipi generici (generics) consentono di scrivere codice indipendente dal tipo specifico. Vengono implementati utilizzando la sintassi delle parentesi angolari:
fn max<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }
Qui T è un tipo generico, limitato dal trait PartialOrd.
I parametri generici vengono dichiarati tramite <T>, ma possono essere limitati con constraint di trait attraverso i due punti, ad esempio, <T: Display>. Questo è un modo per informare il compilatore che solo i tipi per i quali è implementato il trait richiesto possono essere utilizzati.
In Rust si distinguono due forme di dispatch per i generics:
Influenza sul codice macchina: L'uso dei generici con i constraint di trait (senza dyn Trait) porta alla monomorfizzazione: un aumento del binario, ma la massima velocità. L'utilizzo di dyn Trait riduce il binario, ma comporta una perdita di prestazioni.
Domanda: C'è una funzione
fn do_something<T: Debug>(value: &T)
Il compilatore creerà una funzione separata do_something nel codice binario per ogni tipo con cui viene utilizzata, oppure utilizzerà un'implementazione universale?
Risposta errata tipica: Utilizzerà una funzione per tutti i tipi grazie al trait bound.
Risposta corretta: Il compilatore crea copie separate di questa funzione per ogni tipo (monomorfizzazione), poiché il trait bound non rende la funzione generica "universale" attraverso vtable. L'universalità si presenta solo con dyn Trait (dispatch dinamico).
Esempio:
fn print_val<T: std::fmt::Debug>(val: T) { println!("{:?}", val); } // Per ogni chiamata con un tipo diverso verrà creata una propria versione della funzione
Storia
In un progetto con grandi oggetti generici, è emerso che il file binario era diventato notevolmente più grande del previsto. È emerso che la causa era l'ampio utilizzo di funzioni generiche senza vincoli. Le chiamate con decine di tipi hanno portato a una crescita esponenziale delle dimensioni del file eseguibile (code bloat), rilevata solo nella build di rilascio su CI.
Storia
Uno degli sviluppatori accettava un parametro generico con un vincolo di trait, pensando che quel codice funzionasse con la "dispatch dinamica". Questo ha portato a uno spreco di memoria sul server e a una diminuzione delle prestazioni a causa della costante crescita del codice e della sua cache da parte del processore.
Storia
Nella libreria si cercava di utilizzare un trait generico con un tipo Self (ad esempio, il trait Clone) come dyn Trait, cosa non supportata in Rust e che ha portato a un errore di compilazione. Era necessario riscrivere esplicitamente l'interfaccia, altrimenti l'API generica non avrebbe funzionato in modalità dinamica e l'interfaccia avrebbe dovuto essere modificata a livello di compile-time.