impl Trait în Rust

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.

Sintaxă
fn f(x: impl Trait)              // acceptă orice tip cu Trait (generic)
fn g() -> impl Trait             // returnează un tip concret, nenumit
fn numere() -> impl Iterator<Item = u32>
fn creeaza() -> impl Fn(i32) -> i32

Ce face impl Trait

Ca tip de parametru, impl Trait este o scurtătură pentru generics: fn afiseaza(x: impl Display) înseamnă același lucru ca fn afiseaza<T: Display>(x: T). Este util pentru funcții simple, cu un singur parametru generic.

Ca tip de rezultat, impl Trait spune „returnez un singur tip concret, dar nu îți spun care”. Este indispensabil pentru tipuri care nu pot fi scrise de mână: closures (impl Fn(i32) -> i32) și lanțuri de iteratori (impl Iterator<Item = u32>), al căror tip real este foarte lung.

Diferența față de Box<dyn Trait>: impl Trait este rezolvat la compilare, fără alocare și fără vtable, dar funcția trebuie să returneze același tip pe toate ramurile. Dacă ramurile returnează tipuri diferite, ai nevoie de Box<dyn Trait>.

Exemple

Exemplul 1: Returnarea unui iterator

Tipul real al rezultatului este ceva de felul Filter<Map<Range<u32>, ...>, ...>, imposibil de scris. impl Iterator<Item = u32> spune doar ce contează: produce numere u32.

fn ani_bisecti(de_la: u32, pana_la: u32) -> impl Iterator<Item = u32> {
    (de_la..=pana_la).filter(|an| (an % 4 == 0 && an % 100 != 0) || an % 400 == 0)
}

fn main() {
    let lista: Vec<u32> = ani_bisecti(1996, 2030).collect();
    println!("{lista:?}");
    println!("Între 1900 și 2100: {}", ani_bisecti(1900, 2100).count());
}

Rezultat

[1996, 2000, 2004, 2008, 2012, 2016, 2020, 2024, 2028]
Între 1900 și 2100: 49

Exemplul 2: Returnarea unui closure și parametru impl Trait

calculator_tva returnează un closure care ține minte procentul. afiseaza_toate primește orice colecție care se poate parcurge și ale cărei elemente se pot afișa.

use std::fmt::Display;

fn calculator_tva(procent: f64) -> impl Fn(f64) -> f64 {
    move |pret| pret * (1.0 + procent / 100.0)
}

fn afiseaza_toate(elemente: impl IntoIterator<Item = impl Display>) {
    for e in elemente {
        print!("[{e}] ");
    }
    println!();
}

fn main() {
    let tva_standard = calculator_tva(21.0);
    let tva_redus = calculator_tva(11.0);
    println!("{:.2} / {:.2}", tva_standard(100.0), tva_redus(100.0));

    afiseaza_toate(vec!["Iași", "Vaslui"]);
    afiseaza_toate([1.5, 2.25]);
}

Rezultat

121.00 / 111.00
[Iași] [Vaslui] 
[1.5] [2.25]

Greșeli frecvente

  • Returnezi tipuri diferite pe ramuri diferite (if a { Cerc } else { Patrat }) cu -> impl Forma: `if` and `else` have incompatible types. Folosește Box<dyn Forma> sau un enum.
  • Folosești impl Trait în parametri când apelantul are nevoie să aleagă tipul explicit (f::<u8>(...)); cu impl Trait asta nu este posibil, deci folosește <T: Trait>.
  • Te aștepți ca apelantul să poată folosi metode ale tipului concret. Cu -> impl Iterator, apelantul vede doar metodele lui Iterator.

Comenzi înrudite

  • 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.
  • generics și where: 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.
  • 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.
  • closures și move: Funcții anonime, scrise cu |parametri|, care pot folosi variabilele din jur. move le dă proprietatea asupra acelor variabile.
  • 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.