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.