trait objects (dyn Trait) în Rust

Structuri, enum-uri și trait-uri Rust 1.27 (cuvântul dyn)

dyn Trait înseamnă „o valoare de orice tip care implementează trait-ul”, aleasă la rulare. Permite colecții cu tipuri diferite.

Sintaxă
Box<dyn Trait>          // valoare deținută, de orice tip care implementează Trait
&dyn Trait              // referință
Vec<Box<dyn Trait>>     // colecție de tipuri diferite

Ce face trait objects (dyn Trait)

Un vector conține elemente de un singur tip. Dacă vrei un vector cu un cerc, un pătrat și un triunghi, toate cu trait-ul Forma, folosești Vec<Box<dyn Forma>>. dyn Forma este un trait object: „ceva care implementează Forma”, cu tipul exact aflat abia la rulare.

Pentru că tipul exact nu se cunoaște la compilare, nici mărimea lui nu se cunoaște, deci un trait object stă mereu în spatele unui pointer: Box<dyn T>, &dyn T, Rc<dyn T>. Apelul de metodă trece printr-un tabel de funcții (vtable) – dispatch dinamic, puțin mai lent decât generics, dar fără cod duplicat.

Nu orice trait poate fi folosit cu dyn: trait-ul trebuie să fie dyn compatible (numit înainte „object safe”). De exemplu, metodele lui nu pot fi generice și nu pot returna Self. Alege generics (impl Trait, T: Trait) când tipul este același în tot apelul și dyn când ai nevoie să amesteci tipuri.

Exemple

Exemplul 1: O colecție de tipuri diferite

Vectorul conține o reducere procentuală și una fixă, tipuri diferite cu același trait. Fiecare apel aplica ajunge la implementarea tipului real.

trait Reducere {
    fn aplica(&self, pret: f64) -> f64;
    fn descriere(&self) -> String;
}

struct Procent(f64);
struct Fixa(f64);

impl Reducere for Procent {
    fn aplica(&self, pret: f64) -> f64 {
        pret * (1.0 - self.0 / 100.0)
    }
    fn descriere(&self) -> String {
        format!("-{}%", self.0)
    }
}

impl Reducere for Fixa {
    fn aplica(&self, pret: f64) -> f64 {
        (pret - self.0).max(0.0)
    }
    fn descriere(&self) -> String {
        format!("-{} lei", self.0)
    }
}

fn main() {
    let reduceri: Vec<Box<dyn Reducere>> = vec![Box::new(Procent(10.0)), Box::new(Fixa(25.0))];
    let mut pret = 300.0;
    for r in &reduceri {
        pret = r.aplica(pret);
        println!("după {:>8}: {pret:.2} lei", r.descriere());
    }
}

Rezultat

după     -10%: 270.00 lei
după  -25 lei: 245.00 lei

Exemplul 2: &dyn Trait ca parametru și alegerea la rulare

Tipul de notificare este ales la rulare, după un text. Funcția trimite primește &dyn Notificare și nu știe ce tip concret primește.

trait Notificare {
    fn trimite(&self, mesaj: &str) -> String;
}

struct Sms;
struct Email {
    adresa: String,
}

impl Notificare for Sms {
    fn trimite(&self, mesaj: &str) -> String {
        format!("SMS: {mesaj}")
    }
}

impl Notificare for Email {
    fn trimite(&self, mesaj: &str) -> String {
        format!("E-mail către {}: {mesaj}", self.adresa)
    }
}

fn alege(canal: &str) -> Box<dyn Notificare> {
    match canal {
        "sms" => Box::new(Sms),
        _ => Box::new(Email { adresa: "ana@exemplu.ro".to_string() }),
    }
}

fn anunta(n: &dyn Notificare) {
    println!("{}", n.trimite("Comanda ta a fost livrată"));
}

fn main() {
    for canal in ["sms", "email"] {
        let n = alege(canal);
        anunta(n.as_ref());
    }
}

Rezultat

SMS: Comanda ta a fost livrată
E-mail către ana@exemplu.ro: Comanda ta a fost livrată

Greșeli frecvente

  • Scrii Vec<dyn Forma> fără Box: the size for values of type `dyn Forma` cannot be known. Pune trait object-ul în spatele unui pointer.
  • Omiți dyn (Box<Forma>), cum se scria în codul vechi. În ediția 2021 și 2024 este eroare; scrie Box<dyn Forma>.
  • Încerci să folosești cu dyn un trait cu metode generice sau care returnează Self (de exemplu Clone): eroarea the trait cannot be made into an object / is not dyn compatible.

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.
  • 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.
  • struct: Definește un tip nou care grupează mai multe valori cu nume (câmpuri), de exemplu un elev cu nume, clasă și medie.