Az előző leckében megismerted a generikus függvényeket és a trait bound-okat — ez a lecke pontosan ezekre az alapokra épül, csak most egy konkrét, mindenhol előkerülő trait-tel foglalkozunk: az Iterator-ral. Amikor egy for ciklussal bejársz egy vektort, vagy egy range-en végigmész, a színfalak mögött mindig egy Iterator-t implementáló típus dolgozik. Ha megérted, hogyan működik ez a trait, sokkal tudatosabban tudsz majd adatokat feldolgozni — és a következő leckében már saját iterátort is fogsz írni.

Az Iterator trait és a next metódus

A std::iter::Iterator trait a szíve-lelke egy asszociált metódusnak: next. Ez a metódus minden hívásra visszaadja a következő elemet, Some(érték) formában — vagy None-t, ha az iterátor kiürült. Ez egy sima enum, amit már ismersz a match alapokból.

fn main() {
    let numbers = vec![10, 20, 30];
    let mut iter = numbers.iter();

    match iter.next() {
        Some(value) => println!("Első elem: {value}"),
        None => println!("Az iterátor kiürült"),
    }

    match iter.next() {
        Some(value) => println!("Második elem: {value}"),
        None => println!("Az iterátor kiürült"),
    }
}

Fontos, hogy az iterátor mut — minden next() hívás megváltoztatja a belső állapotát (hogy hol áll a bejárásban), ezért kell hozzá mut kötés. A numbers.iter() egy iterátort ad, amelynek elemei &i32 típusúak, hiszen csak kölcsönveszi a vektor elemeit, nem viszi el az ownershipet.

Megjegyzés

A for ciklus valójában pontosan ezt csinálja a háttérben: addig hívja a next()-et, amíg None-t nem kap. A ciklus tehát nem varázslat, csak egy praktikus csomagolás a next() köré.

Iterátor-adapterek: map és filter

Az Iterator trait-nek rengeteg beépített, alapértelmezett metódusa van — ezeket iterátor-adapternek hívjuk, mert egy iterátorból egy másik iterátort csinálnak, anélkül hogy azonnal bármit kiszámolnának. A leggyakoribb kettő a map és a filter.

Mindkettőhöz egy kis, névtelen függvényt kell átadnod, amit closure-nek (lezárásnak) hívunk. A closure szintaxisa |paraméterek| törzs alakú: a | jelek között vannak a paraméterek, utánuk pedig az, hogy mit csináljon a closure minden egyes elemmel. Például a |n| n * 2 closure egy n nevű paramétert vár, és a duplázott értékét adja vissza.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6];

    // .cloned() lemásolja az elemeket, így i32-vel dolgozunk, nem &i32-vel
    let doubled: Vec<i32> = numbers.iter().cloned().map(|n| n * 2).collect();
    println!("{doubled:?}");

    let evens: Vec<i32> = numbers.iter().cloned().filter(|n| n % 2 == 0).collect();
    println!("{evens:?}");
}

A map minden elemre lefuttatja a closure-t, és az eredményekből épít egy új iterátort. A filter closure-je egy bool-t ad vissza: ha true, az elem bekerül a kimenetbe, ha false, kimarad. A .cloned() metódus szintén egy iterátor-adapter — az &i32 elemeket i32-vé alakítja a Clone segítségével, hogy egyszerűbb legyen a típusokkal dolgozni.

A collect és a típus-annotáció szerepe

A map és a filter önmagukban csak leírják, mit kellene csinálni — ahhoz, hogy valódi gyűjteményt (például Vec-et) kapj, meg kell hívnod a collect metódust. A collect generikus: sokféle gyűjteménybe tud "összegyűjteni", ezért a Rust fordítónak sok esetben szüksége van egy explicit típusra, hogy tudja, mit szeretnél kapni.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6];

    let doubled_evens = numbers.iter().cloned().collect();
    println!("{doubled_evens:?}");
}
error[E0282]: type annotations needed for `Vec<_>`
  --> src/main.rs:4:9
   |
4  |     let doubled_evens = numbers.iter().cloned().collect();
   |         ^^^^^^^^^^^^^
   |
help: consider giving `doubled_evens` an explicit type
   |
4  |     let doubled_evens: Vec<_> = numbers.iter().cloned().collect();
   |                      ++++++++++

A hibaüzenet pontosan azt súgja, amit tenned kell: adj típust a változónak. A collect::<Vec<i32>>() szintaxis is működne, de a gyakorlatban a legtöbben inkább a bal oldali let sorba írják oda a típust, mert az olvashatóbb.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6];

    let doubled_evens: Vec<i32> = numbers.iter().cloned().collect();
    println!("{doubled_evens:?}");
}
Tipp

Ha nem szeretnéd a let sort típussal terhelni, használhatod a collect::<Vec<i32>>() formát is — ez ugyanazt az információt adja meg, csak a hívás helyén, nem a változó deklarációjánál.

Lustaság: az adapterek csak végrehajtáskor futnak le

Ez talán a legfontosabb tulajdonsága az iterátor-adaptereknek: lusták (lazy). A map, a filter és a hasonló metódusok önmagukban nem futtatnak le semmit — csak egy "receptet" építenek fel arról, mi történjen, ha valaki elkéri az elemeket. Az egyetlen dolog, ami tényleg elindítja a végrehajtást, egy úgynevezett végrehajtó (consuming) metódus, mint a collect, a for ciklus, vagy a sum.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // Ez a sor önmagában semmit nem futtat le,
    // csak leírja, mit kellene tenni minden elemmel.
    let iterator = numbers.iter().map(|n| {
        println!("Feldolgozom: {n}");
        n * n
    });

    println!("Az iterátor még nem futott le semmit.");

    // Csak itt, a collect hívásakor futnak le a map lépései
    let squares: Vec<i32> = iterator.collect();
    println!("{squares:?}");
}

Ha lefuttatod ezt a programot, látni fogod, hogy a "Feldolgozom: ..." sorok csak a "Az iterátor még nem futott le semmit." üzenet után jelennek meg — pontosan azért, mert a map csak felépíti a láncot, a tényleges munka a collect hívásakor történik.

Jó tudni

A lustaság nem hiba, hanem tervezési döntés: lehetővé teszi, hogy sok adaptert láncolj egymás után anélkül, hogy köztes gyűjteményeket kellene létrehoznod minden lépés után. Ez gyorsabb és kevesebb memóriát használ, mint ha minden map után azonnal collect-elnél.

Gyakorlati láncolt iterátor-műveletek

A gyakorlatban az adaptereket gyakran hosszú láncokba fűzöd: szűrsz, alakítasz, majd összegyűjtesz — mindezt egyetlen kifejezésben, anélkül hogy köztes változókat kellene létrehoznod.

fn main() {
    let result: Vec<i32> = (1..=20)
        .filter(|n| n % 3 == 0)
        .map(|n| n * n)
        .collect();

    println!("{result:?}");
}

Ez a kód az 1-től 20-ig tartó range-ből kiszűri a 3-mal osztható számokat, majd négyzetre emeli őket, végül Vec<i32>-be gyűjti az eredményt. Figyeld meg, hogy egyetlen köztes Vec-et sem hoztunk létre a filter és a map között — az egész lánc egyszerre, a collect hívásakor futott le.

Próbáld ki!

Írj egy programot, amely egy Vec<i32>-ben tárolt számokból kiszámolja az összes páros szám négyzetének összegét. Használd a filter, a map és egy végrehajtó metódust (nézd meg, van-e sum nevű metódusa az iterátoroknak — próbáld ki, hogy a Rust fordító mit mond, ha elfelejtesz típust adni neki). Próbálj meg egyetlen kifejezésben, láncolva megírni az egészet.

Önellenőrzés

Megoldás

Kérdés: Miért kell mut kulcsszó ahhoz a változóhoz, amelyen next()-et hívsz?

Mert a next() metódus megváltoztatja az iterátor belső állapotát (nyilván tartja, hol áll a bejárásban) — ez egy mutáló (&mut self) metódus, ezért a hozzá tartozó változónak mut-nak kell lennie.

Megoldás

Kérdés: Mi a különbség a map és a filter closure-je között, ha a visszatérési típusra gondolunk?

A map closure-je bármilyen típust visszaadhat — ez lesz az új iterátor elemtípusa. A filter closure-jének mindig bool-t kell visszaadnia: true esetén az elem bekerül a kimenetbe, false esetén kimarad, de az elem típusa nem változik.

Megoldás

Kérdés: Ha leírsz egy numbers.iter().map(|n| n * 2) sort, de sosem hívsz rá collect-et vagy hasonló végrehajtó metódust, mi történik a programban?

Semmi — az iterátor-adapterek lusták, tehát a map önmagában csak felépíti a leírást a végrehajtandó lépésről, de a closure teste egyszer sem fut le, amíg valaki tényleg el nem kéri az elemeket (pl. collect, for ciklus, sum).

Összefoglaló

  • Az Iterator trait lelke a next() metódus, amely Some(elem)-et vagy None-t ad vissza — a for ciklus is ezt hívja a háttérben.
  • Az iterátor-adapterek (map, filter, cloned és sok más) új iterátort adnak vissza, de önmagukban semmit nem futtatnak le — ez a lustaság (laziness).
  • A closure-ök (|n| n * 2) apró, inline definiált függvények, amelyeket az adaptereknek adsz át.
  • A collect az a végrehajtó metódus, amely tényleg lefuttatja a láncot, és gyakran igényel explicit típusannotációt, mert generikus.
  • A következő leckében megnézzük, hogyan írhatsz saját iterátort egy saját típusodhoz — ehhez pontosan azt az Iterator trait-et fogod implementálni, amelynek a metódusait most használtad.

További olvasás: