V dnešním článku otevřeme další téma, na které se ve svých emailech často dotazujete. Podíváme se na problematiku streamů neboli datových proudů. Vysvětlíme si, co to streamy jsou, jaké druhy streamů v Delphi existují, jak se používají a k čemu vlastně slouží.
V dnešním článku se podíváme na jeden z možných přístupů, které mohou vést k práci s ukládáním a načítáním informací. Přesně řečeno, zaměříme se na práci s datovými proudy, tzv. streams.
V dnešním článku si o streamech prozradíme nezbytné obecné, teoretické minimum. Za týden se přesuneme do oblasti praxe a ukážeme si, jak využít sreamy k praktickým účelům, tedy jak je vytvářet, jak z nich číst a jak do nich zapisovat. Podíváme se také podrobně na jednotlivé druhy streamů.
Streamy – co to je a k čemu slouží
Co jsou streamy? Jak se liší od jiných metod přistupování k datům? Jak se například liší práce se souborovými streamy od běžné práce se soubory?
Stream si jednoduše představte třeba jako jakousi trubku. Jak funguje běžná trubka? Z jedné strany do ní sypeme materiál (typicky vodu :-)), druhou stranu trubky kamsi připojíme. Připojíme ji typicky do místa, kam chceme materiál (vodu) dovést. Po tomto nezbytném (avšak jednorázovém) úvodu už jen velmi jednoduše lijeme do trubky další a další vodu, která spolehlivě a bezpečně protéká do místa, kam ji chceme dostat.
Počítačový stream, především ten realizovaný v Delphi, o němž se budeme v tomto článku bavit, funguje zcela analogicky. Jedná se jakousi obdobu trubky, kterou kamsi připojíme (do operační paměti, do souborového disku, do síťového socketu apod.) a prostřednictvím které budeme přelévat data tam, kam je chceme dostat (nebo odtud, odkud je chceme získat).
Na začátku celých manévrů zvolíme stream podle toho, kam chceme data „prolévat“. Pokud chceme pracovat s diskovým souborem, zvolíme souborový stream (TFileStream). Pokud chceme pracovat s operační pamětí, zvolíme paměťový stream (TMemoryStream). Potom už jenom pomocí trubky sypeme data do cílového umístění, případně je naopak z cílového umístění sosáme ven.
Následně jen velmi jednoduše trubku připojíme (tj. nakonfigurujeme stream – třeba mu sdělíme název souboru, do kterého se má připojit) a lijeme do ní kbelík za kbelíkem (tj. zapisujeme do stremu buffer za bufferem). Voda protéká streamem do cílového umístění (data jsou streamem přenášena a zapisována do cílového souboru).
Pomocí streamů lze přistupovat k načítání a ukládání informací do celé řady umístění, například souborový stream (TFileStream) umožňuje pracovat se soubory, paměťový stream (TMemoryStream) umožňuje pracovat s operační pamětí, řetězcový stream (TStringStream) umožňuje zapisovat do řetězců apod. Existují i streamy pro zapisování do databázových BLOB polí, existují streamy pro zapisování do síťových socketů apod.
Velkou výhodou streamů je, že práce se všemi druhy je do určité míry unifikovaná, jinak řečeno – metody použité pro ukládání informací na disk lze bez velkých modifikací použít i pro ukládání informací do operační paměti apod. Vždy je pouze nutné učinit rozhodnutí týkající se použitého streamu a zvolit odpovídající třídu pro práci s odpovídajícím umístěním (paměť, disk).
Třídy pro práci se streamy poskytují sadu metod umožňujících zapisovat a načítat data. Dostupných „streamových“ tříd existuje celá řada, mají však jedno společné: všechny jsou odděděny od společného předka – od třídy TStream. Z tohoto faktu také plyne to, co jsem si uvedli před okamžikem - všechny streamy mají do určité míry podobné chování a ovládání.
Použití streamů k zapisování a ke čtení dat
Všechny druhy streamů, bez ohledu na to, jestli pracují s operační pamětí, diskovým souborem, textovým řetězcem, síťovým socketem či jakýmkoliv jiným umístěním, sdílí několik společných metod, prostřednictvím kterých čteme a zapisujeme data ze a do streamů. Existuje více druhů metod pro každou z operací. Jednotlivé druhy metod se vzájemně odlišují podle toho, zda:
- vrací počet přečtených nebo zapsaných bajtů,
- vyžadují dopředu známý počet bajtů,
- při selhání vyhazují pouze chybové hlášení nebo výjimku
Pojďme se nyní ve stručnosti podívat na společné metody pro čtení a zapisování údajů.
Začneme metodou z nejzákladnějších, a to metodou pro čtení údajů ze streamu. Nikoho asi nepřekvapí, že se jedná o metodu Read. Tato metoda přečte specifikovaný počet bajtů ze streamu. Začne přitom číst na aktuální pozici ve streamu. Přečtené znaky se zapisují do specifikovaného bufferu. Po dokončení operace způsobí metoda Read změnu aktuální pozice ve streamu podle toho, kolik znaků se ve skutečnosti přečetlo.
Můžete si to představit jednoduše tak, že každý stream (podobně jako například diskové soubory) disponuje jakýmsi vnitřním „ukazovátkem“, které si pamatuje aktuální pozici, na níž skončila poslední operace (např. kde se naposledy přestalo číst). Tohoto ukazovátka můžeme snadno využít při příštích operacích, kdy potřebujeme pokračovat ve čtení, pokračovat v zápisu nebo provést jakoukoliv jinou operaci související nějak s poslední známou pozicí ve streamu.
Vraťme se však k metodě Read. Její funkční prototyp vypadá takto:
function Read(var Buffer; Count: Longint): Longint;
Vysvětlovat parametry funkce Read asi není úplně nezbytné, proto jen velmi stručně:
- Buffer je paměťový buffer, do něhož mají být uloženy přečtené znaky. Obvykle se jedná o nějakou proměnnou, o ukazatel na paměťové místo (pozor, nezapomeňte dopředu alokovat paměť) nebo o jiné analogické umístění.
- Count je počet znaků, které hodláme ze streamu přečíst
Funkce Read je užitečná v případě, kdy dopředu neznáme celkový počet znaků v souboru. Důvod spočívá v tom, že Read po dokončení čtení vrátí počet znaků, které byly skutečně přečteny. Je zřejmé, že v některých případech může být tento počet nižší než hodnota specifikovaná parametrem Count: k tomu dojde v situaci, kdy celkový počet znaků v souboru je nižší než počet požadovaný parametrem Count.
Další užitečná základní metoda se nazývá Write: jedná se o analogii k funkci Read. Podobně jako Read slouží Write k zapsání stanoveného počtu znaků do streamu. Zapisování začne na aktuální pozici ve streamu.
Dodejme, že aktuální pozice ve streamu je přístupná prostřednictvím vlastnosti Position, kterou každý stream disponuje.
Funkční prototyp funkce Write vypadá takto:
function Write(const Buffer; Count: Longint): Longint;
Poté, co Write zapíše do streamu, posune se aktuální pozice o počet skutečně zapsaných znaků. Funkce následně vrátí počet skutečně zapsaných znaků. Tento počet může být nižší než počet znaků, který jsme požadovali, a to ve dvou případech: buď pokud při zapisování dojdeme ke konci bufferu a nebo pokud stream nemůže akceptovat žádné další znaky.
Pokud jde o operace čtení a zápisu, existují ještě další dvě užitečné metody, které provádějí v podstatě podobnou činnost jako Read a Write, částečně se však liší. Jedná se o procedury ReadBuffer a WriteBuffer. Jejich zásadní rozdíl oproti výše popsaným Read a Write spočívá v tom, že nevrací počet skutečně přečtených (zapsaných) bajtů. To je ostatně patrné už z toho, že se jedná o procedury, nikoliv o funkce.
Takže, procedury ReadBuffer a WriteBuffer jsou užitečné v okamžiku, kdy počet bajtů je dopředu znám (a je navíc vyžadován), například při čtení nebo zapisování struktur. Protože ani jedna z procedur nevrací skutečně zpracovaný počet znaků, musí existovat nějaký mechanismus, jak aplikaci sdělit, že se „něco stalo“ a že skutečně zpracovaný počet znaků je menší než to, co jsme si představovali. Obě procedury k tomuto účelu používají mechanismus výjimek.
Jinak řečeno, ReadBuffer a WriteBuffer vyhodí výjimku EReadError nebo EWriteError v případě, kdy skutečný počet přečtených nebo zapsaných znaků nesouhlasil s předpokládanou délkou. Podívejme se na prototypy obou procedur:
procedure ReadBuffer(var Buffer; Count: Longint);
procedure WriteBuffer(const Buffer; Count: Longint);
Vidíme, že parametry těchto procedur jsou stejné jako parametry funkcí Read a Write. Pro zájemce o podrobnosti snad ještě dodáme, že k fyzickému provedení operace čtení, resp zápisu volají procedury ReadBuffer, resp. WriteBuffer stejně funkce Read, resp. Write popsané výše.
Další druhy streamů
Nejčastější použití streamů v případě začátečníků asi bude čtení, resp. zapisování ze, resp. do souborů, případě operační paměti nebo řetězců. Všechna tato použití si ukážeme v příštích dílech našeho seriálu. Existuje však celá řada dalších možných využití, povšechně reprezentována specifickými druhy streamů.
K jedněm z nejspecifičtějších streamů patří napříkald streamy pr čtení, resp. zapisování komponent. Spíše jen pro zajímavost si všimněme existence specializovaných metod sloužících k tomuto účelu: jedná se o metody ReadComponent a WriteComponent. Tyto metody jsou velmi užitečné v případě, kdy chceme například ve své aplikaci uložit komponenty a hodnoty jejich vlastností v okamžiku, kdy je vytváříme za běhu programu (v run-time).
Pokud už máte v Delphi něco za sebou, jistě jste postřehli, že velmi specifickým Delphi útvarem jsou řetězce. K práci s řetězci existují v mnoha různých oblastech specifické metody: výjimkou nebudou ani streamy.
Pokud jde o práci se streamy, musíme si dávat pozor zejména na předávání řetězců do funkcí, které je budou dále zpracovávat. Přesněji řečeno, musíme si dávat pozor na správnou syntaxi. V předchozí části článku jsme si ukázali několik metod, které všechny používaly parametr Buffer. Parametr Buffer pro „čtecí“ metody byl typu var (protože se do něj zapisovalo), parametr Buffer pro „zápisové“ metody byl typu const (protože z něho se čte). V obou případech se však jedná o netypované parametry, takže metoda pracuje vlastně s adresou proměnné.
Zaměřme se nyní na řetězce. Nejobvyklejším způsobem běžné práce s řetězci jsou tzv. long strings. Problém je v tom, že pokud se pokusíme předat long string jakožto parametr Buffer, nedostaneme očekávaný (a už vůbec ne správný) výsledek. Dlouhé řetězce v sobě obsahují svou velikost, počet referencí a ukazatel na samotné znaky v řetězci. V důsledku toho, dereferencování long stringu (tedy získání ukazatele na proměnnou typu long string) nevrátí očekávaný pointer. Co musíme udělat?
Problémy vyřešíme, když nejprve přetypujeme string na Pointer nebo na PChar a teprve následně jej dereferencujeme.
Podívejme se na příklad:
procedure CastString;
var
fs: TFileStream;
const
s: string = `Nazdar`;
begin
fs := TFileStream.Create(`test.txt`, fmCreate or fmOpenWrite);
fs.Write(s, Length(s)); // tento příkaz není korektní
fs.Write(PChar(s)^, Length(s)); // zde je správný způsob
end;
Na závěr
Dnes jsme si pověděli základní informace o streamech jakožto o metodě práce s daty. Řekli jsme si, že existuje celá řada streamů a že všechny streamy jsou odděděny od třídy TStream. V důsledku toto disponují všechny streamy podobnými metodami pro základní operace – především pro čtení a zapisování dat.
V příštím článku budeme o trochu konkrétnější a ukážeme si, jak se streamem prakticky pracovat.