Typ Text
Wirthův Pascal odrážel ještě nerozvinutý obor skladování dat té doby. Pro praktické použití souborového systému, který nám OS nabízí, můžeme samozřejmě použít přímé volání služeb OS. Pro představu tady je zjednodušená hlavička knihovnou Windows exportované funkce pro zápis dat:
function WriteFile(hFile: integer; // uchopitko (cislo) souboru const Buffer; // data nNumberOfBytesToWrite: integer; // kolik zapsat var lpNumberOfBytesWritten: integer): boolean;
Nad podobnými funkcemi, které v podstatě nabízejí jen různé formy stěhování binárních dat mezi soubory a proměnnými, máme již od ranných verzí jazyka TurboPascal k dispozici také prostředky pro práci s datovými a textovými soubory na úrovni jazyka Pascal. Stále ale když pracujeme se souborem, musíme respektovat, že je to objekt spadající do kompetence OS a v jazyce máme jen vrátka, skrze která nám je dovoleno provádět se soubory užitečné operace pohodlněji, mnohá omezení však zůstávají.
Nejdříve budeme uvažovat vstup z a výstup do textového souboru. Dnes již je jakýkoli textový soubor posloupností bytů, nikoli štítků či bloků na pásce nebo magnetickém bubnu. Textový soubor je soubor, ve kterém, co byte to znak nebo řídící znak. (Pozn. byte ve smyslu nejmenšího kvanta zapsatelného do souboru, 8 bitů, nikoli jako předdefinovaný typ ObjectPascalu pro krátké neoznaménkované číslo.) Poslední dobou ani to již není tak prosté háčky, čárky a hyeroglyfy všech národů se spojily v Unicode a tak příští generace studentů programování bude muset překousnout 16-bitové znaky! Pokud neplatí jednoduchá rovnost co byte (word) to znak, mluvíme souboru binárním. Příkladem může být třeba soubor nějakého obrázku: Nehomogenní skládačka z binární hlavičky souboru následované bloky, každý se svojí hlavičkou a komprimovanými daty .... Práci s takovým souborem si necháme na jindy.
V Pascalu je textový soubor (tedy klíč od oněch ony dveří do světa) zastoupen proměnnou typu Text. Takto vytvoříme (nebo pokud existuje zkrátíme na nulovou délku) textový soubor s názvem 'Soubor.txt' v běžném adreáři a zapíšeme do něj krátkou zprávu:
var T: Text; begin Assign(T,'Soubor.txt'); Rewrite(T); Writeln(T,'Toto je prni radek souboru, druhy zustane prazdny!'); Close(T); end.
Co se souborem můžeme dělat nám velmi určuje OS, on je za něj zodpovědný. Proto i proměnné representující soubory zdědí jistá omezení. Především, je zakázáno přiřazení do proměnné typu Text. Navíc nemá pro nás přístupnou vnitřní strukturu, takže nemůžeme psát něco jako T.Name := 'Soubor.txt'. Zbývá tak jen třetí způsob práce s proměnnou typu Text, a to použití této proměnné jako parametr procedury nebo funkce. I zde jsme omezeni, nesmíme předávat soubor hodnotou. Proto všechny oprace se soubory budou mít formu volání procedur, kde jako první parametr bude proměnná typu Text. Následujícími procedurami, které podobně jako třeba ReadLn umí překladač aniž se musíme doprošovat nějaké knihovny, můžeme ovládat práci s textovými soubory.
Assign(TextVar, Retezec) ... Přiřazení jména.
Musíme zadat platné jméno na daném stroji.
Rewrite(TextVar) ... Vytvoř soubor nulové délky a otevři
pro zápis. Pokud existuje zahoď co je v něm.
Append(TextVar) ... Otevři soubor pro zápis na jeho konci.
Připisuje se za původní obsah. Musí existovat.
Reset(TextVar) ... Otevři soubor pro čtení. Musí před
tím existovat.
Close(TextVar) ... Uzavři soubor.
Mezi voláním Rewrite a Close (případně Append a Close) můžeme psát do soubor pomocí příkazů Write a Writeln, viz výše uvedený příklad. Mezi voláním Reset a Close můžeme ze souboru číst pomocí Read a ReadLn.
Pokud se vyskytne problém dostaneme buď
Runtime error 2 at 0040432E
nebo o něco hezčí okénko s oznámením, že se nám
počítač a tvůrci programu omlouvají....
Jak víme chování se dá v případě chyby ovlivňovat
pomocí direktiv. Jako obvykle, máme na výběr mezi krátkou a
dlouhou formou:
Samo od sebe (default) je hlídání nastveno na {$I+} t.j.
{$IOCHECKS ON} a dostaneme výše uvedené chování.
V případě, že je hlídání "vypneme" {$I-} t.j.
{$IOCHECKS OFF}, pozastaví se vykonávání operací vstupu a
výstupu až do doby, než se na zeptáme funkce
function IOResult : integer; // je k dispozici vždy, nepotřebujeme žádnou knihovnu
Ta nám vrátí 0, pokud nenastaly problémy. Pokud vrátí něco jiného, znamená to že nastaly problémy a podle hodnoty se můžeme dohadovat, co se stalo. Především jsou ale od okažiku volání IOResult opět povoleny operace se soubory.
Pokud tedy chceme např. vědět, zda nějaký soubor existuje, můžeme výše uvedeného chování využít a psát
function SouborExistuje(const Jmeno) : boolean; var T:text; begin {$IOCHECKS OFF} Assign(T,Jmeno); Reset(T); // pokud není, vznikne chyba a pozastaví se další operace Close(T); // nesmíme zapomenout, kdyby existoval SouborExistuje := IOResult=0; {$IOCHECKS ON} end;
Jak tušíme, důvody proč nám OS nedovolí ze souboru číst můžou být různé a výše uvedená funkce opravdu jen testuje jestli je operace Reset pro daný soubor povolena. Pokud potřebujeme detilnější informace o souboru, nezbyde než se zeptat OS přímo.
Příkazy Write a WriteLn
Obecně se vyskytují ve dvou variantách:
Write(PromTypuSoubor, Vyraz, ...)
nebo
Write(Polozka, ...)
V prvním případě je první parametr proměnná typu Text a výstup probíhá do příslušného souboru, jeho obsah je tentýž jaký v druhém případě skončí na standardním výstupu (konsoli, přesměrovaný někam...).
Jak vidíme Write a Writeln nejsou obyčejné procedury a nemají ani obyčejné parametry. Především jich mohou mít libovoný počet výrazů mnoha typů (celočíselné, reálné, řetězce, znaky, logické). Dále za výrazem mohou následovat i dva další celočíselné výrazy oddělené od toho prvního dvojtečkami, které určují formát výstupu, tedy způsob, jímž se hodnota převede to textové podoby. Za první dvojtečkou se nachází šířka pole, do níž je třeba hodnotu vypsat. Pro reálné výrazy může za druhou dvojtečkou následovat počet desetinných míst.
Vyzkoušejte jak se chovají následující příkazy textového výstupu.
Writeln(Pi); Writeln(Pi:5); Writeln(Pi:10); Writeln(Pi:15); Writeln(Pi:20); Writeln(Pi:25);
Writeln(Pi); Writeln(Pi:30); Writeln(Pi:30:0); Writeln(Pi:30:4); Writeln(Pi:30:8); Writeln(Pi:30:12); Writeln(Pi:30:16); Writeln(Pi:30:20); Writeln(Pi:30:24);
Podobně můžeme psát
Write('':i);
a vypsat tak i-krát mezeru.
Writeln je znám dobrou vůlí vypsat výstup i když se hodnota nevejde do předepsané šířky. V tom případě se vypíše v minimální nutné šířce. Bohužel to většinou příliš nepomůže:
for i := 995 to 1005 do Writeln(i:4,i*i:7,i*i*i:10); vypíše 995 990025 985074875 996 992016 988047936 997 994009 991026973 998 996004 994011992 999 998001 997002999 100010000001000000000 100110020011003003001 100210040041006012008 100310060091009027027 100410080161012048064 100510100251015075125
Pro další strojové zpracování jsou asi data stejně nepoužitelná i když se Write tak moc snažil.
Příkazy Read a ReadLn
Fukce slouží (opět ve dvou variantách) ke čtení ze
standardního vstupu nebo z textového souboru. Ve druhém
případě musí být první parametr typu text.
Protože procedura Readln vždy po načtení všech parametrů
přeskočí na začátek nového řádku,
vyzkoumáme. coz následujícího vstupu skočí ve kterých
proměnných dále uvedených příkladů
1 2 3 4 5
10 20 30
Budeme uažovat dva kousky kódu.
Read(a,b); // a=1 b=2 Read(c); // c=3 zbytek se nepoužije
a
ReadLn(a,b); // a=1 b=2 zbytek řídku se přeskočí ReadLn(c); // c=10 Naproti tomu pro vstupní data ve formě
1 2 3 4
se výsledky obou kousků kódu lišit nebudou.
Samozřejmě podobně je to s načítáním hodnot reálných
proměnných, formát zatím popíšeme příklady
1
+1
-1
1.23
1E10
1E+10
1.003E-10
atp. dle libosti, ovšem
1.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000E10
si ale vyloží jako 1.0 takže všeho s mírou.
Read/Readln ale umí načíst i řetězce a například následující kód načte do pole celý textový soubor
program ReadText; var T : array of string; i,s : integer; begin SetLength(T,100); i:=-1; while not eof do begin i:=i+1; if (i>High(T)) then SetLength(T,2*i); Readln(T[i]); end; SetLength(T,i+1); // ted spoctu pocet znaku, abych uz s tim nactenym souborem neco udelal s := 0; for i := 0 to High(T) do s:=s+length(T[i]); Writeln('Soubor obsahuje ',s,' znaku na ',High(T)+1,' radcich'); Readln; end.
Pokud jej spustíme a jako vstup mu přesměrujeme nějaký delší textový soubor, dozvíme se, že
C:\Projects\prog\pokusy>test<"C:\Program Files\Borland\Delphi6\Source\Rtl\Win\Windows.pas" Soubor obsahuje 1113550 znaku na 30848 radcich
Poznámka: Pokud Předěláte SetLength(T,2*i) na SetLength(T,i+1) zjistíte, že je program pro soubor s 30000 řádky nepracuje, ale jen rachtá diskem. Jakkoli jsme zatím tomu tak neříkali, program vytváří dynamické datové struktury a my narážíme na nejvlastnější problémy dynamických datových struktur, tzv. potřebu sběru odpadků. [...Pár slov na přednášce.. Čtěnáře poznámek odkazuji na pokračování přednášky.] Tak jak je program napsán nepřesáhnou odpadky po zvětšování pole dvojnásobek velikosti užitečných dat pro uložení informací o řádcích (+100), což můžeme na naší neinformatické úrovni považovat za dostatečný úspěch.
Pro základní vstup číselných hodnot jsou použitelné přímo procedury Read/ReadLn. Pro složitěji formátovaný vstupní text je nejjednodušší si vstup načíst do řetězce, v něm nalézt polohu číselného podřetězce a ten převést na číslo pomocí funkce val. Viz velký příklad na řetězce výše. Za zmínku stojí, že narozdíl od běhové chyby nebo mechanismu oznamování chyb přes IOResult, umí val vrátit přímo index znaku, který mu zabránil v převední řetězce na číslo a ošetření případné chyby tak může být méně drsné.