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 match kifejezés (az Option<T> kezeléséhez)
  • impl blokkok és trait-metódusok megírása
  • generikák és trait bound-ok alapjai
  • for ciklus és range-ek

Az új fogalmak, amelyeket ma megtanulsz:

  • az Iterator trait kézi implementálása – hogyan írd meg a next metódust úgy, hogy az valóban egy értéksorozatot generáljon
  • az IntoIterator trait – hogyan tedd a saját típusodat for ciklusban közvetlenül iterálhatóvá
Megjegyzés

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:

  1. Ha remaining == 0, adj vissza None-t – ezzel jelzed, hogy a sorozat véget ért.
  2. Egyébként számítsd ki az új next értéket az aktuális kettőből (current + next), csökkentsd a remaining-et, majd add vissza a régi current értéket becsomagolva Some-ba.
Tipp

Ü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átor
  • type IntoIter – melyik konkrét iterátor-típust használjuk
  • into_iter(self) -> Self::IntoIter – hogyan hozzuk létre ezt az iterátort
Tipp

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}");
    }
}
Jó tudni

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):

  1. Írj egy Countdown struct-ot, amely egy megadott számtól visszaszámol nulláig, és implementáld rá az Iterator trait-et. Gondold át, mi történik, ha a kezdőérték már 0 – hány elemet ad vissza az iterátor?
  2. Bővítsd ki a Playlist típust úgy, hogy az IntoIterator-t egy &Playlist referenciára is implementáld (tehát a songs mezőt ne mozgasd el, csak kölcsönvedd). Gondolkodj el azon, milyen Item típust adna vissza ez az implementáció, és miért lenne más, mint az eredeti.
Figyelem

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.