closures și move în Rust

Funcții anonime, scrise cu |parametri|, care pot folosi variabilele din jur. move le dă proprietatea asupra acelor variabile.

Sintaxă
let f = |x| x + 1;
let g = |a: i32, b: i32| -> i32 { a * b };
let h = move || println!("{text}");   // ia proprietatea variabilelor folosite
fn aplica(f: impl Fn(i32) -> i32) { ... }

Ce face closures și move

Un closure este o funcție fără nume, scrisă pe loc: |x| x * 2. Parametrii stau între bare verticale, tipurile sunt de obicei deduse, iar corpul este o expresie sau un bloc. Closures se folosesc peste tot cu iteratorii (map, filter, sort_by_key) și ca funcții transmise altor funcții.

Spre deosebire de fn, un closure poate captura variabile din jur. Rust alege cel mai blând mod de captură: prin referință (dacă doar citește), prin referință mutabilă (dacă modifică) sau prin mutare (dacă trebuie să dețină valoarea). De aici cele trei trait-uri: Fn (poate fi apelat de oricâte ori, doar citește), FnMut (modifică ce a capturat) și FnOnce (consumă ce a capturat, deci poate fi apelat o dată).

move înaintea closure-ului îl obligă să ia proprietatea asupra tuturor variabilelor folosite. Este necesar când closure-ul trăiește mai mult decât funcția care l-a creat: un fir de execuție (thread::spawn(move || ...)) sau un closure returnat dintr-o funcție. Pentru tipurile Copy (numere), move le copiază.

Exemple

Exemplul 1: Closures cu iteratori

prag este capturat prin referință în closure-ul din filter. sort_by primește un closure cu doi parametri care compară două produse.

fn main() {
    let mut produse: Vec<(&str, f64)> = vec![("lapte", 9.5), ("pâine", 5.0), ("cafea", 32.9), ("unt", 12.4)];
    let prag = 10.0;

    let scumpe: Vec<&str> = produse.iter()
        .filter(|(_, pret)| *pret > prag)
        .map(|(nume, _)| *nume)
        .collect();
    println!("Peste {prag} lei: {scumpe:?}");

    produse.sort_by(|a, b| b.1.total_cmp(&a.1));
    println!("De la cel mai scump: {produse:?}");

    let cu_tva = |pret: f64| -> f64 { (pret * 1.21 * 100.0).round() / 100.0 };
    println!("Cafea cu TVA: {}", cu_tva(32.9));
}

Rezultat

Peste 10 lei: ["cafea", "unt"]
De la cel mai scump: [("cafea", 32.9), ("unt", 12.4), ("lapte", 9.5), ("pâine", 5.0)]
Cafea cu TVA: 39.81

Exemplul 2: FnMut: un closure care modifică ce a capturat

inregistreaza modifică total, deci este FnMut și trebuie declarat cu let mut. După ultima folosire a closure-ului, total poate fi citit din nou direct.

fn main() {
    let mut total = 0;
    let mut inregistreaza = |suma: i32| {
        total += suma;
        println!("+{suma} lei");
    };
    inregistreaza(120);
    inregistreaza(45);
    inregistreaza(300);
    println!("Total: {total} lei");
}

Rezultat

+120 lei
+45 lei
+300 lei
Total: 465 lei

Exemplul 3: move: closure trimis într-un alt fir de execuție

Firul nou poate trăi mai mult decât main, deci nu poate împrumuta elevi. move mută vectorul în closure; firul îl prelucrează și returnează rezultatul prin join.

use std::thread;

fn main() {
    let elevi = vec![String::from("Ana"), String::from("Mihai"), String::from("Ioana")];
    let fir = thread::spawn(move || {
        let mut nume = elevi.join(", ");
        nume.push_str(&format!(" ({} elevi)", elevi.len()));
        nume
    });
    let rezultat = fir.join().unwrap();
    println!("{rezultat}");
}

Rezultat

Ana, Mihai, Ioana (3 elevi)

Greșeli frecvente

  • Trimiți un closure într-un fir fără move: eroarea E0373 closure may outlive the current function, but it borrows `x`. Adaugă move.
  • Folosești o variabilă după ce a fost mutată într-un closure move. Dacă ai nevoie de ea în ambele locuri, clonează-o înainte (let copie = x.clone();) și mută copia.
  • Declari fără mut un closure care modifică ceva: cannot borrow `f` as mutable. Un closure FnMut se ține într-o variabilă let mut.

Comenzi înrudite

  • fn: Declară o funcție. Tipul fiecărui parametru este obligatoriu, iar ultima expresie fără punct și virgulă este valoarea returnată.
  • 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.
  • ownership (proprietatea asupra valorilor): Regula de bază a lui Rust: fiecare valoare are un singur proprietar, iar când proprietarul dispare, valoarea este eliberată automat.
  • for … in: Parcurge pe rând fiecare element al unui interval, al unei colecții sau al unui iterator.
  • borrowing și referințe (& și &mut): O referință (&) împrumută o valoare fără să o ia de la proprietar. Regula: oricâte referințe de citire SAU una singură de modificare.