🦀 rust.hu
gyakorlat30 perc•modul 2/3 · lecke 6/9

Iterátoros adatfeldolgozás: map, filter, collect

Állíts össze érthető map–filter–collect láncokat, figyeld meg az iterátorok lustaságát, és dönts kölcsönzés vagy tulajdonátadás mellett. A zárófeladat hibás szám esetén részleges eredmény helyett Result hibát ad.

Cél és előfeltétel

Egy adatfeldolgozó feladatban gyakran ugyanaz a három lépés követi egymást: kiválasztjuk a megfelelő elemeket, átalakítjuk őket, majd összegyűjtjük az eredményt. Ebben a leckében ezt iterátorokkal fogalmazod meg. A cél, hogy a bemenet, a kimenet és a hibakezelés világos legyen.

Előfeltétel az A Rust alapjai útvonal, valamint a korábbi gyakorlati leckék a Vec, a String, az Option és a Result használatáról. Az előző HashMap-gyakorlat riportjában már találkoztál map és collect hívásokkal; most megnézzük, mikor dolgoznak ténylegesen, és milyen tulajdonjogot kérnek. Minden példa teljes program, Rust 2024 editionnel és kizárólag standard könyvtári elemekkel.

A lánc lépései

A filter egy feltétel alapján engedi tovább az elemeket, a map pedig minden továbbengedett elemből új értéket készít. A collect a lánc eredményét egy választott gyűjteménybe gyűjti. A cél típusát általában érdemes leírni, mert ugyanaz a bejárás többféle eredménybe is összegyűjthető. A Vec<i32> jelölés olvasáskor is segít.

fn main() {
    let bemenet = vec![3_i32, -2, 0, 4];
    let eredmeny: Vec<i32> = bemenet.iter().copied()
        .filter(|szam| *szam > 0)
        .map(|szam| szam * 2)
        .collect();
    assert_eq!(eredmeny, vec![6, 8]);
    assert_eq!(bemenet, vec![3, -2, 0, 4]);
}

A sorok sorrendje itt egy történetet mond el: kölcsönzött bejárás, másolható számok átvétele, szűrés, kétszerezés, összegyűjtés. A példában a kis számok miatt a szorzás belefér az i32 tartományába. Általános bemenetnél a számtartományt vagy az ellenőrzött aritmetikát külön meg kell tervezni; az iterátor nem változtatja meg a szorzás szabályait.

A függőleges vonalak közötti név egy lezárás paramétere. A filter a vizsgált elem referenciáját kapja, ezért az érték összehasonlításánál *szam szerepel. A map ebben a láncban már saját számot vesz át. Ha elbizonytalanodsz, bontsd a láncot sorokra, és minden lépés mellé írd oda fejben az átadott elem típusát.

Mikor történik a feldolgozás?

Az iterátor adapterei lusták: a feldolgozó lezárások törzse akkor fut, amikor valaki lekéri az elemeket. A lánc összeállítása önmagában nem hajtja végre a teljes átalakítást. A következő példában egy számlálóval láthatóvá tesszük ezt. A Cell itt csak demonstrációs segédeszköz; a szokásos adatfeldolgozó függvénynek nincs szüksége külső számlálóra.

use std::cell::Cell;

fn main() {
    let lepesek = Cell::new(0);
    let terv = (1..=3).map(|szam| {
        lepesek.set(lepesek.get() + 1);
        szam * 2
    });
    assert_eq!(lepesek.get(), 0);
    let eredmeny: Vec<_> = terv.take(2).collect();
    assert_eq!(eredmeny, vec![2, 4]);
    assert_eq!(lepesek.get(), 2);
}

A take(2) miatt csak két elemre van szükség. Egy szűrős lánc két eredményéhez ennél több bemeneti elem vizsgálata is kellhet. A lustaságból tehát nem következik általános „két lépés” ígéret: mindig a konkrét láncot nézd. Az iterátorok működését a Rust Book vonatkozó fejezete követhető példákkal vezeti be.

Kié marad a bemenet?

A Vec saját iter() hívása &T elemeket ad, ezért a vektor megmarad. A copied() csak Copy típusoknál készít saját értékeket; a String nem ilyen. Ha nevekből csak valamilyen tulajdonság kell, például a skalárértékek száma, olvasd őket referencián keresztül. Másolni csak azért, hogy egy bejárás leforduljon, többnyire felesleges kerülő.

A tulajdonolt Vec into_iter() hívása elfogyasztja a vektort, és átadja az elemeit. Ezt válaszd, ha a régi gyűjteményre már nincs szükséged. A megkülönböztetés a hívott érték típusától is függ: egy referencián meghívott into_iter() nem ugyanazt jelenti, mint a saját vektoron. Most tudatosan az utóbbi esetet használjuk. A módosító iter_mut() külön lehetőség, amikor ugyanazokba az elemekbe akarsz visszaírni.

1. feladat: szűrt, biztonságos négyzetek

Írj paros_negyzetek függvényt &[i32] bemenetre. Csak a nulla és száz közé eső páros számok négyzeteit adja vissza, az eredeti sorrendet megtartva. A tartomány két széle is elfogadott; így a legnagyobb szorzat tízezer, amely belefér az eredménytípusba. A bemenet maradjon használható, az üres vagy teljesen elutasított sorozatból pedig üres eredmény legyen.

Megoldás
fn paros_negyzetek(adatok: &[i32]) -> Vec<i32> {
    adatok.iter().copied()
        .filter(|szam| (0..=100).contains(szam) && *szam % 2 == 0)
        .map(|szam| szam * szam)
        .collect()
}

fn main() {
    let adatok = vec![-4, -1, 0, 2, 5, 100, 102];
    assert_eq!(paros_negyzetek(&adatok), vec![0, 4, 10_000]);
    assert_eq!(adatok.len(), 7);
    assert!(paros_negyzetek(&[]).is_empty());
    assert!(paros_negyzetek(&[-2, 3, 101, i32::MAX]).is_empty());
}

A szűrés a szorzás előtt áll. A kizárt nagy számot ezért nem próbáljuk előbb megszorozni, és csak utána ellenőrizni.

2. feladat: a nevek átalakítása tulajdonátadással

Készíts tiszta_nevek függvényt, amely átvesz egy Vec<String> értéket. Vágja le a nevek szélső whitespace karaktereit, hagyja el az így üressé váló elemeket, és őrizze meg a többi elem sorrendjét. A hívó átadja a régi gyűjteményt. Az új, levágott szövegek létrehozásához továbbra is készülnek saját String értékek.

Megoldás
fn tiszta_nevek(nevek: Vec<String>) -> Vec<String> {
    nevek.into_iter()
        .map(|nev| nev.trim().to_owned())
        .filter(|nev| !nev.is_empty())
        .collect()
}

fn main() {
    let nevek = vec![" Réka ".to_owned(), "\t".to_owned(), "Áron".to_owned()];
    assert_eq!(tiszta_nevek(nevek), vec!["Réka", "Áron"]);
    assert!(tiszta_nevek(Vec::new()).is_empty());
    assert!(tiszta_nevek(vec!["".to_owned(), "  ".to_owned()]).is_empty());
}

A map előbb egységesíti a széleket, majd a filter már a ténylegesen visszaadandó neveket vizsgálja. A betűméretet és a Unicode-alakot megőrizzük.

3. feladat: minden szám vagy egy hiba

Írj szamok függvényt &[&str] bemenetre. Siker esetén az összes elem u32 értékét kérjük. Az első hibás elemnél az egész művelet adjon hibát, részleges eredmény nélkül. Ne használj filter_map és ok() párost: az csendben elhagyná a rossz adatot. A whitespace levágása most nem része a szerződésnek, tehát a szóközös szám is hibás.

Megoldás
use std::num::ParseIntError;

fn szamok(bemenetek: &[&str]) -> Result<Vec<u32>, ParseIntError> {
    bemenetek.iter().map(|szoveg| szoveg.parse::<u32>()).collect()
}

fn main() {
    assert_eq!(szamok(&["2", "10"]).unwrap(), vec![2, 10]);
    assert_eq!(szamok(&[]).unwrap(), Vec::<u32>::new());
    assert!(szamok(&["1", "hiba", "3"]).is_err());
    assert!(szamok(&["-1"]).is_err());
    assert!(szamok(&[" 2"]).is_err());
    assert_eq!(szamok(&["4294967295"]).unwrap(), vec![u32::MAX]);
    assert!(szamok(&["4294967296"]).is_err());
}

A célként megadott Result<Vec<u32>, ParseIntError> határozza meg az összegyűjtés szabályát. Sikeres elemekből vektor épül, az első Err pedig megszakítja a bejárást. A Rust By Example hibás elemekről szóló része más döntési lehetőségeket is összehasonlít; itt a hibát megőrző viselkedést választottuk.

Elfogadási feltételek

  1. A négyzetes szűrés elfogadja a nullát és a százat, elutasítja a tartományon kívüli számokat, és a bemeneti vektort megtartja.
  2. A lustaságot mutató program számlálója fogyasztás előtt nulla, a take(2) után kettő; a névtisztítás kezeli az üres elemeket is.
  3. A számfeldolgozás üres bemenetre sikeres üres vektort, hibás vagy túl nagy számra Err értéket ad; egyetlen elemet sem hagy el csendben.

Összefoglaló és továbblépés

Egy jó iterátorlánc adatfeldolgozási szerződést ír le. Megmondja, mely elemek maradnak meg, mivé alakulnak, ki birtokolja őket, és mi történik hiba esetén. A következő leckében fájlból érkező adatokkal dolgozol tovább, ahol az olvasási hibák és a feldolgozási hibák megkülönböztetése is fontossá válik.

gyors önellenőrzés

Próbáld ki, mire emlékszel.

Válaszold meg a kérdéseket, majd ellenőrizd a válaszaidat és olvasd el a magyarázatokat. A válaszok az oldal elhagyásakor törlődnek.

1. Melyik vektor készül el a szűrés és az átalakítás után?
fn main() {
    let adatok = vec![1_i32, 2, 3, 4];
    let eredmeny: Vec<_> = adatok.iter().copied()
        .filter(|szam| *szam % 2 == 0)
        .map(|szam| szam * 2)
        .collect();
    println!("{eredmeny:?}");
}
2. Összeállítasz egy map láncot, amelynek lezárása számlálót növel, de még nem kérsz egyetlen elemet sem. Mikor fut a lezárás törzse?
3. Egy Vec<String> neveiből skalárérték-számokat gyűjtesz, utána az eredeti neveket is ki szeretnéd írni. Melyik bejárás illik ehhez?
4. A Result célú collect az első hibánál megáll. Mit ír ki a program a beolvasott elemek számáról?
use std::cell::Cell;

fn main() {
    let darab = Cell::new(0);
    let eredmeny: Result<Vec<u32>, _> = ["2", "hiba", "5"].iter()
        .map(|szoveg| {
            darab.set(darab.get() + 1);
            szoveg.parse::<u32>()
        })
        .collect();
    println!("{} {}", eredmeny.is_err(), darab.get());
}

0 / 4 kérdés megválaszolva