Cél és előfeltételek
Az előző leckében, "Az Iterator trait" címűben megismerted, hogyan működik a next metódus, és hogyan épülnek rá a lusta iterátor-adapterek, mint a map, filter és collect. Ez a lecke ennek a folytatása: most nem egy meglévő iterátort használsz, hanem egy saját típust alakítasz iterátorrá, a nulláról.
Ehhez a következő fogalmakra lesz szükséged, amelyeket korábban már megtanultál:
- struct definíció és mezők
- enum és a
matchkifejezés (azOption<T>kezeléséhez) implblokkok és trait-metódusok megírása- generikák és trait bound-ok alapjai
forciklus és range-ek
Az új fogalmak, amelyeket ma megtanulsz:
- az Iterator trait kézi implementálása – hogyan írd meg a
nextmetódust úgy, hogy az valóban egy értéksorozatot generáljon - az IntoIterator trait – hogyan tedd a saját típusodat
forciklusban közvetlenül iterálhatóvá
Ez egy gyakorlat-lecke: minden lépésnél előbb próbálkozz magad, és csak akkor nyisd ki a "Megoldás" blokkot, ha elakadtál, vagy ellenőrizni szeretnéd az eredményt.
Előkészítés
Hozz létre egy új projektet:
cargo new custom_iterator
cd custom_iterator
Erre a leckére semmilyen külső crate-re nincs szükség, a Cargo.toml maradhat az alapértelmezett:
[package]
name = "custom_iterator"
version = "0.1.0"
edition = "2024"
[dependencies]
Minden kódot az src/main.rs-be fogsz írni.
1. lépés – A generátor struct
Az első feladat, hogy megírj egy Fibonacci nevű struct-ot, amely a Fibonacci-sorozat első n elemét fogja generálni. A struct-nak három mezőre lesz szüksége: az aktuális és a következő elemre (u64 típussal), valamint egy számlálóra, hogy hányat generáltunk már.
Feladat: definiáld a struct-ot, és írj hozzá egy new asszociált függvényt, amely count: usize paramétert vár, és a Fibonacci-sorozatot 0-tól és 1-től indítja.
Megoldás
struct Fibonacci {
current: u64,
next: u64,
remaining: usize,
}
impl Fibonacci {
fn new(count: usize) -> Self {
Fibonacci {
current: 0,
next: 1,
remaining: count,
}
}
}
fn main() {
let fib = Fibonacci::new(5);
println!("{} elem lesz generálva", fib.remaining);
}
2. lépés – A next metódus
Most jön a lényeg: implementáld az Iterator trait-et a Fibonacci struct-ra. A trait-nek egy asszociált típusa van (type Item), és egyetlen kötelező metódusa, a next, amely Option<Self::Item>-et ad vissza.
A logika:
- Ha
remaining == 0, adj visszaNone-t – ezzel jelzed, hogy a sorozat véget ért. - Egyébként számítsd ki az új
nextértéket az aktuális kettőből (current + next), csökkentsd aremaining-et, majd add vissza a régicurrentértéket becsomagolvaSome-ba.
Ügyelj a mezők sorrendjére: előbb mentsd el a visszaadandó értéket egy lokális változóba, mert a self.current és self.next frissítése közben könnyen elronthatod a sorrendet.
Megoldás
struct Fibonacci {
current: u64,
next: u64,
remaining: usize,
}
impl Fibonacci {
fn new(count: usize) -> Self {
Fibonacci {
current: 0,
next: 1,
remaining: count,
}
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
return None;
}
let value = self.current;
let new_next = self.current + self.next;
self.current = self.next;
self.next = new_next;
self.remaining -= 1;
Some(value)
}
}
fn main() {
let fib = Fibonacci::new(8);
for value in fib {
println!("{value}");
}
}
Figyeld meg, hogy a for value in fib sor magyarázat nélkül is működik, pedig a Fibonacci struct-ra sosem implementáltunk IntoIterator-t! Ennek az az oka, hogy a standard library minden Iterator-ra ad egy úgynevezett blanket implementációt az IntoIterator trait-ből, amely az iterátort egyszerűen önmagára képezi. Tehát: minden iterátor egyben IntoIterator is – ez a for ciklus működésének az alapja.
3. lépés – Amikor a saját típusod nem maga az iterátor
A Fibonacci esetében a struct volt az iterátor. De gyakran van egy másik helyzet: van egy típusod, amely egy gyűjteményt csomagol be (például egy Vec<T>-t), és azt szeretnéd, hogy for ciklusban iterálható legyen, anélkül, hogy a belső gyűjteményt közvetlenül elérhetővé tennéd.
Feladat: definiálj egy Playlist struct-ot egyetlen privát mezővel, songs: Vec<String>. Írj hozzá egy new függvényt, amely egy Vec<String>-et fogad. Ezután implementáld rá az IntoIterator trait-et úgy, hogy egy for ciklus közvetlenül végig tudjon menni a dalok nevein (érték szerint, nem referenciával).
Az IntoIterator trait-nek három része van:
type Item– mit ad vissza az iterátortype IntoIter– melyik konkrét iterátor-típust használjukinto_iter(self) -> Self::IntoIter– hogyan hozzuk létre ezt az iterátort
Nem kell a nulláról megírnod egy új iterátor-struct-ot: a Vec<String>-nek már van saját IntoIterator implementációja (std::vec::IntoIter<String>), csak azt kell visszaadnod a belső Vec-en meghívva az into_iter-t.
Megoldás
struct Playlist {
songs: Vec<String>,
}
impl Playlist {
fn new(songs: Vec<String>) -> Self {
Playlist { songs }
}
}
impl IntoIterator for Playlist {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.songs.into_iter()
}
}
fn main() {
let playlist = Playlist::new(vec![
"Első szám".to_string(),
"Második szám".to_string(),
"Harmadik szám".to_string(),
]);
for song in playlist {
println!("Lejátszás: {song}");
}
}
Az Iterator és az IntoIterator két külön trait, más a felelősségük. Az Iterator maga az állapotot tartó, lépésenkénti generátor (van next metódusa). Az IntoIterator viszont azt írja le, hogyan lehet egy típusból egy iterátort előállítani – ezt hívja meg a for ciklus a színfalak mögött.
4. lépés – A saját iterátor tesztelése beépített adapterekkel
Mivel a Fibonacci implementálja az Iterator trait-et, minden korábban tanult adapter (map, filter, collect) automatikusan használható rajta – ugyanúgy, mintha egy Vec vagy egy range iterátorán dolgoznál.
Feladat: generálj 10 Fibonacci-számot, szűrd ki csak a páros elemeket, majd szorozd meg mindegyiket 10-zel, végül gyűjtsd össze egy Vec<u64>-be, és írd ki az eredményt.
Megoldás
struct Fibonacci {
current: u64,
next: u64,
remaining: usize,
}
impl Fibonacci {
fn new(count: usize) -> Self {
Fibonacci {
current: 0,
next: 1,
remaining: count,
}
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
return None;
}
let value = self.current;
let new_next = self.current + self.next;
self.current = self.next;
self.next = new_next;
self.remaining -= 1;
Some(value)
}
}
fn main() {
let even_times_ten: Vec<u64> = Fibonacci::new(10)
.filter(|value| value % 2 == 0)
.map(|value| value * 10)
.collect();
println!("{even_times_ten:?}");
}
Ha ez lefut, láthatod, hogy a saját, kézzel írt iterátorod ugyanúgy viselkedik, mint bármelyik beépített: a filter és a map lusta marad addig, amíg a collect (vagy egy for ciklus) tényleg nem kényszeríti ki az értékek kiszámítását.
Teljes referencia-megoldás
Az alábbi kód a teljes leckét egyetlen fordítható programba fűzi össze: a Fibonacci iterátort és a Playlist típust is bemutatja.
struct Fibonacci {
current: u64,
next: u64,
remaining: usize,
}
impl Fibonacci {
fn new(count: usize) -> Self {
Fibonacci {
current: 0,
next: 1,
remaining: count,
}
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
return None;
}
let value = self.current;
let new_next = self.current + self.next;
self.current = self.next;
self.next = new_next;
self.remaining -= 1;
Some(value)
}
}
struct Playlist {
songs: Vec<String>,
}
impl Playlist {
fn new(songs: Vec<String>) -> Self {
Playlist { songs }
}
}
impl IntoIterator for Playlist {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.songs.into_iter()
}
}
fn main() {
// Saját Iterator: a Fibonacci-sorozat generálása és feldolgozása adapterekkel.
let even_times_ten: Vec<u64> = Fibonacci::new(10)
.filter(|value| value % 2 == 0)
.map(|value| value * 10)
.collect();
println!("Páros Fibonacci-elemek tízszerezve: {even_times_ten:?}");
// Saját IntoIterator: a Playlist típus for ciklusban iterálható.
let playlist = Playlist::new(vec![
"Első szám".to_string(),
"Második szám".to_string(),
"Harmadik szám".to_string(),
]);
for song in playlist {
println!("Lejátszás: {song}");
}
}
Bónusz-kihívások
Ha szeretnéd tovább gyakorolni, próbálkozz meg ezekkel (megoldás nélkül, hogy magad birkózz meg velük):
- Írj egy
Countdownstruct-ot, amely egy megadott számtól visszaszámol nulláig, és implementáld rá azIteratortrait-et. Gondold át, mi történik, ha a kezdőérték már 0 – hány elemet ad vissza az iterátor? - Bővítsd ki a
Playlisttípust úgy, hogy azIntoIterator-t egy&Playlistreferenciára is implementáld (tehát asongsmezőt ne mozgasd el, csak kölcsönvedd). Gondolkodj el azon, milyenItemtípust adna vissza ez az implementáció, és miért lenne más, mint az eredeti.
Ha a Fibonacci-hoz hasonló, számláló nélküli, végtelen iterátort írnál, ügyelj arra, hogy egy for ciklus vagy egy collect() sosem fog leállni – mindig gondoskodj egy leállási feltételről, vagy korlátozd az elemek számát valamilyen módon.
Összefoglalás és mi jön legközelebb
Ma megtanultad, hogyan implementálhatod kézzel az Iterator trait-et egy saját struct-on a next metódus megírásával, és hogyan különbözik ettől az IntoIterator trait, amely azt írja le, hogyan válik egy típus for ciklusban iterálhatóvá. Azt is láttad, hogy a két trait szorosan összefügg: minden Iterator automatikusan IntoIterator is a standard library blanket implementációja miatt.
A modul következő leckéje egy "Ellenőrző kvíz", amelyben összefoglalod és leellenőrzöd, mennyire ülnek le a generikákkal és iterátorokkal kapcsolatos ismereteid, mielőtt továbblépnétek a haladó útvonal következő témáira.