generics și where în Rust

Structuri, enum-uri și trait-uri Rust 1.0 (generice constante: 1.51)

Scrii o funcție sau un tip o singură dată, pentru orice tip T; limitele (T: Trait) spun ce trebuie să știe să facă T.

Sintaxă
fn nume<T: Trait>(x: T) -> T { ... }
fn nume<T, U>(x: T, y: U) -> String
where
    T: Display + Clone,
    U: Debug,
{ ... }
struct Cutie<T> { valoare: T }
impl<T> Cutie<T> { ... }
fn f<const N: usize>(a: [i32; N])

Ce face generics și where

Generics înseamnă cod parametrizat după tip. fn maxim<T: PartialOrd>(lista: &[T]) -> &T funcționează pentru numere, texte sau orice tip care se poate compara. T este un parametru de tip, iar : PartialOrd este o limită (trait bound): fără ea, compilatorul nu te lasă să folosești > pe valori de tip T.

Mai multe limite se scriu cu + (T: Display + Clone). Când lista devine lungă, ele se mută într-o clauză where după semnătură, ca funcția să rămână lizibilă. Structurile și enum-urile pot fi și ele generice: Vec<T>, Option<T>, HashMap<K, V> sunt exemple din biblioteca standard.

Generics nu costă nimic la rulare: compilatorul generează o versiune separată pentru fiecare tip folosit (monomorfizare), la fel de rapidă ca una scrisă de mână. Parametrii const N: usize fac tipuri generice după un număr, de exemplu array-uri de orice lungime.

Exemple

Exemplul 1: Funcție generică cu limită

Aceeași funcție maxim găsește cea mai mare notă, cea mai mare temperatură (f64) și ultimul oraș în ordine alfabetică.

fn maxim<T: PartialOrd>(lista: &[T]) -> &T {
    let mut cel_mai_mare = &lista[0];
    for element in lista {
        if element > cel_mai_mare {
            cel_mai_mare = element;
        }
    }
    cel_mai_mare
}

fn main() {
    println!("{}", maxim(&[7, 10, 9, 8]));
    println!("{}", maxim(&[21.5, 24.1, 19.8]));
    println!("{}", maxim(&["Iași", "Arad", "Timișoara", "Cluj"]));
}

Rezultat

10
24.1
Timișoara

Exemplul 2: Structură generică și clauză where

Pereche<T> ține două valori de același tip. Metoda afiseaza_mai_mare există doar pentru tipurile care se pot compara și afișa, condiție scrisă în where.

use std::fmt::Display;

struct Pereche<T> {
    a: T,
    b: T,
}

impl<T> Pereche<T> {
    fn new(a: T, b: T) -> Self {
        Pereche { a, b }
    }
}

impl<T> Pereche<T>
where
    T: PartialOrd + Display,
{
    fn afiseaza_mai_mare(&self) {
        if self.a >= self.b {
            println!("mai mare: {}", self.a);
        } else {
            println!("mai mare: {}", self.b);
        }
    }
}

fn main() {
    Pereche::new(15, 42).afiseaza_mai_mare();
    Pereche::new("Ana", "Bogdan").afiseaza_mai_mare();
}

Rezultat

mai mare: 42
mai mare: Bogdan

Exemplul 3: Parametru generic constant

N este lungimea array-ului, cunoscută la compilare. Aceeași funcție primește array-uri de lungimi diferite, fără să le transforme în slice-uri.

fn medie<const N: usize>(note: [f64; N]) -> f64 {
    note.iter().sum::<f64>() / N as f64
}

fn main() {
    println!("{:.2}", medie([9.0, 8.5, 10.0]));
    println!("{:.2}", medie([6.0, 7.0, 8.0, 9.0, 10.0]));
}

Rezultat

9.17
8.00

Greșeli frecvente

  • Folosești un operator pe T fără limita potrivită: binary operation `>` cannot be applied to type `T`. Compilatorul îți spune ce limită să adaugi (T: PartialOrd).
  • Faci totul generic de la început. Generics complică semnăturile; începe cu tipuri concrete și generalizează când ai cu adevărat mai multe tipuri.
  • Pui limite pe definiția structurii (struct Cutie<T: Display>). De obicei este mai bine să pui limitele doar pe blocurile impl sau funcțiile care au nevoie de ele.

Comenzi înrudite

  • trait: Definește un set de metode pe care un tip promite să le aibă. Seamănă cu interfețele din alte limbaje și stă la baza generics.
  • impl Trait: Scrie „un tip care implementează Trait” fără să-l numești: ca parametru este o scurtătură pentru generics, ca rezultat ascunde tipul exact.
  • trait objects (dyn Trait): dyn Trait înseamnă „o valoare de orice tip care implementează trait-ul”, aleasă la rulare. Permite colecții cu tipuri diferite.
  • lifetimes (durate de viață, 'a): Adnotări ca ‘a care spun compilatorului cât timp trebuie să rămână valide referințele, ca să nu existe niciodată o referință spre memorie eliberată.
  • type (alias de tip și tip asociat): Dă un nume nou unui tip existent (alias), ca să scurtezi tipuri lungi; în trait-uri, declară un tip asociat.