A bemenet lehet adatfolyam is

Az előző két leckében a teljes bemenet egyetlen szöveg volt. Fájlnál vagy hálózati kapcsolatnál azonban a következő sor olvasása külön művelet, amely hibával is végződhet. Most a feldolgozó egy BufRead megvalósítást kap, így nem kell tudnia, hogy az adat fájlból, standard bemenetből vagy memóriából érkezik. A példák Cursor segítségével, beágyazott bájtokból dolgoznak: ugyanazt az adatfeldolgozó függvényt tudjuk tesztelni külső fájl nélkül.

Előfeltétel a típusos hibák leckéje. Megtartjuk a korábbi nyelvtant: a teljes komment és üres sor kihagyható, az első egyenlőségjel bontja a mezőket, üres mező nincs. A különbség az olvasás életciklusában és a szöveg tulajdonlásában jelenik meg. Az adatfolyam nem feltétlenül visszatekerhető; a következő kérésre kapott hiba ezért nem törli el azt a tényt, hogy előtte már sorokat olvastunk.

A soriterátor eredménye is Result

A BufRead::lines minden elemként egy olvasási eredményt ad. Sikerben saját Stringet kapunk a sorvégjel nélkül, hiba esetén io::Error értéket. A Result figyelmen kívül hagyása nem puszta kényelmi rövidítés: adatvesztést rejthet el. A filter_map(Result::ok) ebben a feldolgozóban hibás döntés lenne, mert a rosszul olvasható sort csendben kihagyná, miközben a többi rekord érvényes konfigurációnak látszana.

A beolvasott String az adott cikluslépéshez tartozik. Ha abból kölcsönzött mezőket készítesz, nem tárolhatod őket a cikluson túl úgy, hogy a sort közben eldobod. A tartós rekordokba ezért most saját String kerül. Ez tudatos költség, nem a kölcsönzés kudarcának jele. Másik megoldásként a hívó soronként feldolgozhatná a nézeteket még a sor megszűnése előtt, de akkor eltérő felületet terveznénk.

Mit jelent itt a folyamatosság?

A generikus bemenet elsősorban függőségi határ. A feldolgozónak egy olvasható sorfolyam viselkedésére van szüksége, nem egy konkrét fájlnévre. Ezért a fájl megnyitása és a felhasználói üzenet megjelenítése később a program indítási rétegébe kerülhet. A belső függvény ugyanaz marad akkor is, ha a teszt egy rövid bájttömböt, az alkalmazás pedig egy pufferelt fájlt ad át neki. A két adapter közös felülete nem jelenti, hogy minden meghibásodási tulajdonságuk azonos.

A stream végének felismerését is a megfelelő rétegben kell elvégezni. A read_line nulla olvasott bájtja befejezést jelez, míg egy üres sorhoz tartozó sortörés beolvasása még tényleges adatolvasás. Ha csak a puffer trimelt ürességére figyelnél, az első üres sor után elveszítenéd az összes későbbi konfigurációt. A feladat ezért tartalmaz üres sort két értelmes rekord között és sortörés nélküli utolsó rekordot is.

Az eredmény birtoklását az határozza meg, meddig akarja használni a hívó. A soronként kiszámolt darabszámhoz nem szükséges megtartani a teljes szöveget. A később újra felhasznált konfigurációhoz viszont saját rekordok vagy hosszabb életű közös forrás kellenek. Az adatfolyam kiolvasása után a hívó ne kapjon olyan nézetet, amely egy már felszabadított ciklusváltozóra mutatna. A Rust élettartam-ellenőrzése ezt nem engedi meg, a felület megtervezése azonban továbbra is a fejlesztő feladata.

A tesztekben figyeld külön az olvasást és a feldolgozást. A rossz UTF-8 bájtsor az olvasási réteget teszi próbára, az egyenlőségjel nélküli érvényes szöveg a parser szerkezetellenőrzését. Mindkettő hibát eredményezhet, de eltérő okból. Ha csak a végső siker vagy hiba számát vizsgálnád, könnyen rejtve maradna, hogy az egyik teszt valójában el sem jutott a vizsgálni kívánt rétegig.

1. feladat: számold meg az adatsorokat

A függvény soronként olvasson, trimelés után hagyja ki az üres sorokat és a teljes kommenteket, majd adja vissza az adatsorok számát. A szerkezeti érvényességet ebben a feladatban még ne ellenőrizze. Bármely olvasási hibát továbbítson; az érvénytelen UTF-8 sor ne számítson pusztán üres sornak. Próbáld ki a CRLF sortörést és az utolsó sorvégjel nélküli adatot is.

Megoldás
use std::io::{self, BufRead, Cursor};

fn adatsorok<R: BufRead>(reader: R) -> io::Result<usize> {
    let mut darab = 0;
    for sor in reader.lines() {
        let sor = sor?;
        let tiszta = sor.trim();
        if !tiszta.is_empty() && !tiszta.starts_with('#') { darab += 1; }
    }
    Ok(darab)
}
fn main() {
    let input = Cursor::new("# fejlécek\r\n\r\nnév=Réka\r\nlimit=4".as_bytes());
    assert_eq!(adatsorok(input).unwrap(), 2);
    assert_eq!(adatsorok(Cursor::new(b"".as_slice())).unwrap(), 0);
    let hiba = adatsorok(Cursor::new(vec![0xff, b'\n'])).unwrap_err();
    assert_eq!(hiba.kind(), io::ErrorKind::InvalidData);
}

2. feladat: használj újra egy sortárolót

Most a leghosszabb trimelt adatsor Unicode-skalárértékekben mért hosszát keresd. Használj egyetlen String puffert, amelyet minden read_line hívás előtt kiürítesz. A nulla visszatérési érték jelenti az adatfolyam végét; a pusztán sortörésből álló sor nem ugyanaz. A megjegyzéseket hagyd ki. A hosszmérés pontosan chars-alapú legyen: grafémaszámot ebből a feladatból sem állítunk elő.

Megoldás
use std::io::{self, BufRead, Cursor};

fn leghosszabb<R: BufRead>(mut reader: R) -> io::Result<usize> {
    let mut puffer = String::new();
    let mut maximum = 0;
    loop {
        puffer.clear();
        if reader.read_line(&mut puffer)? == 0 { break; }
        let sor = puffer.trim();
        if sor.is_empty() || sor.starts_with('#') { continue; }
        maximum = maximum.max(sor.chars().count());
    }
    Ok(maximum)
}
fn main() {
    assert_eq!(leghosszabb(Cursor::new(" á \n\n# hosszú komment\nabc".as_bytes())).unwrap(), 3);
    assert_eq!(leghosszabb(Cursor::new("# csak komment\n".as_bytes())).unwrap(), 0);
    assert_eq!(leghosszabb(Cursor::new("e\u{301}".as_bytes())).unwrap(), 2);
}

A read_line a meglévő String végéhez fűz, ezért a clear elhagyásával egymás után felhalmoznánk a sorokat. A clear a tartalmat üríti, a kapacitás újra felhasználható. Ez nem állít felső határt egyetlen sor méretére. A nagy vagy nem megbízható bemenethez külön sorméret- és teljesbemenet-korlátot kell tervezni; a soronkénti olvasás önmagában nem ilyen korlát.

3. feladat: olvasásból saját rekordok

Készíts olyan függvényt, amely saját String mezőket tartalmazó rekordlistát ad. Az eredeti sorszám minden rekordban és minden hibában szerepeljen. A hiányzó elválasztó és az üres mezők InvalidData típusú hibát jelentsenek, az olvasás eredeti io::Error értékét viszont ne alakítsd puszta szöveggé. Hiba esetén az egész feldolgozás Err eredménnyel záruljon. Az érték további egyenlőségjelei megmaradnak, és a bemenet végén nem kötelező sortörés.

Megoldás
use std::{error::Error, fmt, io::{self, BufRead, Cursor}};
#[derive(Debug)]
struct SorHiba { sor: usize, forras: io::Error }
impl fmt::Display for SorHiba {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "a(z) {}. sor feldolgozása sikertelen", self.sor)
    }
}
impl Error for SorHiba {
    fn source(&self) -> Option<&(dyn Error + 'static)> { Some(&self.forras) }
}
fn parok<R: BufRead>(reader: R) -> Result<Vec<(usize, String, String)>, SorHiba> {
    let mut eredmeny = Vec::new();
    for (index, olvasott) in reader.lines().enumerate() {
        let sor = index + 1;
        let szoveg = olvasott.map_err(|forras| SorHiba { sor, forras })?;
        let tiszta = szoveg.trim();
        if tiszta.is_empty() || tiszta.starts_with('#') { continue; }
        let (k, v) = tiszta.split_once('=').ok_or_else(|| SorHiba {
            sor, forras: io::Error::new(io::ErrorKind::InvalidData, "hiányzó ="),
        })?;
        let (k, v) = (k.trim(), v.trim());
        if k.is_empty() || v.is_empty() {
            return Err(SorHiba { sor, forras: io::Error::new(io::ErrorKind::InvalidData, "üres mező") });
        }
        eredmeny.push((sor, k.to_owned(), v.to_owned()));
    }
    Ok(eredmeny)
}
fn main() {
    let lista = parok(Cursor::new(" # fejléc\r\n\r\nnév=Réka\r\njel=a=b".as_bytes())).unwrap();
    assert_eq!(lista, vec![(3, "név".to_owned(), "Réka".to_owned()), (4, "jel".to_owned(), "a=b".to_owned())]);
    let hiba = parok(Cursor::new(b"a=2\n\xff\n".as_slice())).unwrap_err();
    assert_eq!(hiba.sor, 2);
    assert_eq!(hiba.forras.kind(), io::ErrorKind::InvalidData);
    assert!(hiba.source().is_some());
    assert_eq!(parok(Cursor::new(b"# fejlec\n=4".as_slice())).unwrap_err().sor, 2);
}

A Cursor dokumentációja megmutatja, hogyan használható memóriabeli adat az I/O-felületeken. A sikeres Cursor-próba a feldolgozó működését ellenőrzi, nem egy valódi lemez meghibásodási viselkedését. A harmadik feladat Vec eredménye az összes rekordot tárolja, ezért a memóriaigénye az eredmény méretével együtt növekszik, még ha a beolvasás soronként történik is.

Elfogadási feltételek

  • Az üres bemenet, CRLF és sorvégjel nélküli utolsó adat működik; a hibás UTF-8 nem tűnik el sikeres eredményben.
  • A read_line ciklus nem halmozza a korábbi sorokat a pufferben, és nem téveszti össze az üres sort az adatfolyam végével.
  • A rekordok saját szöveget birtokolnak, a hibákban pontos sorszám és hozzáférhető forrás marad.

Továbblépés

A soronkénti felület függetleníti a feldolgozást az adat származási helyétől, de a helyes hibakezelés és tulajdonlás továbbra is a mi döntésünk. Írd le külön, mit tárolsz egyetlen sor idejére és mit tartasz meg a teljes eredményhez. A következő leckében összehasonlítjuk az első hibánál megálló és az összes hibát összegyűjtő feldolgozást, változatlanul pontos forráspozíciókkal.