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.
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:?}");
}
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.
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
Iteratortrait lelke anext()metódus, amelySome(elem)-et vagyNone-t ad vissza — aforciklus 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
collectaz 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
Iteratortrait-et fogod implementálni, amelynek a metódusait most használtad.
További olvasás: