Главная arrow Статьи arrow Использование потоков данных (TStream)  
03.09.2010
Главное меню
Главная
Лаборатория
Книги по дельфи
Инструменты
Статьи
Видео
Юмор
Поиск
Новости
Лента новостей
Новости Delphi
Новости проекта
Новости инструментов
Разные новости
База знаний Delphi
Общие вопросы
Windows, WinAPI
Графика, Звук
IDE Delphi
Консоль
Каталог@Mail.ru - каталог ресурсов интернет
комментарии
Использование потоков данных (TStream) Версия для печати
Рейтинг: / 12
ХудшаяЛучшая 
Написал Alexeis   
26.05.2008

Для чего эта статья.

Статья для тех кто до сих пор игнорирует этот весьма эффективный и быстрый метод обработки данных.

Чтобы легче себе представить что такое поток можно представить себе магнитную ленту. Чтобы прочитать что-то с нее мы опускаем считывающую головку и читаем данные определенного объема или пока не дойдем до конца ленты. Тоже самое при записи. Если запись производить несколько раз (блок за блоком), то информация будет ложиться на ленту последовательно блок за блоком. После записи или чтения позиция считывающей головки останется там же где она осталось после предыдущей операции. Вот и все ограничения! Внутри это может быть реализовано совершенно по разному. В перерывах между чтением данных данных хранилище может шириться (например подгружаться из интернета или из диска), можно сделать бесконечную ленту, т.е. при переходе в конец потока подставить ему начало хранилища, можно реализовать принцип конвейера, т.е. хранилище маленькое, но в него постоянно подкладывают в конец (очередь), а забирают всегда поочереди. Это весьма удобный механизм при работе с одиночными объектами, коллекциями, массивами объектов и даже деревьями.

Все начинается именно с TStream. В делфи есть много классов его наследников таких как TMemoryStream, TFileStream, TResourceStream, TCompressionStream, TStringStream и другие. Их объединяют механизмы удобной быстрой загрузки, сохранения, добавления и обрезки данных. Но их объединяет не только логика но и реализация, реализация класса TStream. Забегая наперед скажу что сам класс "недореализован", т.е. нельзя создать объект такого класса (так как он абстрактный), но кой чего он умеет.

Как это ни странно, но начнем именно с того чего он не умеет. Не умеет он то что касается низкоуровневого доступа к самим данным. Оно и понятно, наследников много всем не угодишь, но при этом он "знает" как это должны делать его наследники. Под знает я имею ввиду что часть его методов виртуальные и даже абстрактные (без реализации).

Работает это так. Пусть мы создаем собственный класс. Наш класс занимается обработкой данных. Может получать их из некоторого источника и после обработки сохранять или передавать дальше по цепочке. Однако источников данных может быть много, например из файла, из ресурса, или просто из буфера в памяти. Если решать это задачу в лоб, то нам понадобилось бы создавать 3 разных метода, для получения данных из перечисленных источников. Используя класс TStream, нам достаточно сделать всего один. Часто его объявляют так

 
Procedure TMyClass.LoadFromStream(AStrem : TStream);

Это совсем не значит, что мы ожидаем что нам в функцию передадут объект класса TStream (Это невозможно! Такой класс создать нельзя в принципе). Вместо TStream, нам подойдет любой полноценный класс где реализованы все возможности ввода и вывода (например TFileStream, TResourceStream и т.д.). Это возможно потому, что все эти классы являются наследниками TStream. А мы у себя считая что работаем TStream, на самом деле будем вызывать методы того класса который в действительности был создан (это то что я имел ввиду когда писал, что TStream "знает" как это должны делать его наследники). Это и есть полиморфизм на практике т.е. наследник и предок реагируют похоже на одинаковые события. Чтобы такое было возможно метод должен объявлен как виртуальный или абстрактный. Если метод был объявлен как виртуальный, то производимое методом действие будет заменено действием того объекта, который был реально создан, если же он объявлен как абстрактный, то изначально предок ничего и не делал, но по его названию можно догадаться что бы он хотел сделать. Мы уже говорили, что механизм виртуальных методов позволяет подменять действие, но бывает так что класс предназначается только для подмены своего наследника (именно такой TStream), тогда метод предка никогда не будет вызываться, вот как раз в таком случае виртуальные методы делают абстрактными (т.е. без своей реализации).

После прочтения предыдущего абзаца у вас уже достачно знаний, чтобы разобрать структуру класса

 
TStream = class(TObject)
  private
    function GetPosition: Int64;
    procedure SetPosition(const Pos: Int64);
    procedure SetSize64(const NewSize: Int64);
  protected
    function GetSize: Int64; virtual;
    procedure SetSize(NewSize: Longint); overload; virtual;
    procedure SetSize(const NewSize: Int64); overload; virtual;
  public
    function Read(var Buffer; Count: Longint): Longint; virtual; abstract;
    function Write(const Buffer; Count: Longint): Longint; virtual; abstract;
    function Seek(Offset: Longint; Origin: Word): Longint; overload; virtual;
    function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; overload; virtual;
    procedure ReadBuffer(var Buffer; Count: Longint);
    procedure WriteBuffer(const Buffer; Count: Longint);
    function CopyFrom(Source: TStream; Count: Int64): Int64;
    function ReadComponent(Instance: TComponent): TComponent;
    function ReadComponentRes(Instance: TComponent): TComponent;
    procedure WriteComponent(Instance: TComponent);
    procedure WriteComponentRes(const ResName: string; Instance: TComponent);
    procedure WriteDescendent(Instance, Ancestor: TComponent);
    procedure WriteDescendentRes(const ResName: string; Instance, Ancestor: TComponent);
    procedure WriteResourceHeader(const ResName: string; out FixupInfo: Integer);
    procedure FixupResourceHeader(FixupInfo: Integer);
    procedure ReadResHeader;
    property Position: Int64 read GetPosition write SetPosition;
    property Size: Int64 read GetSize write SetSize64;
  end;

Начнем анализ класса с методов Read и Write. Они абстрактные, значит реализованы в наследнике. Служат они для чтения данных из текущей позиции потока наследника TStream и записи в него соответственно. Первый параметр это сам буфер данных (внимание не указатель, а сама переменная буфера!). Второй размер буфера в байтах. Результат число переданных данных. Это базовые методы ввода/вывода. На них построена вся передача данных. Их удобство заключается в том, что при правильном использовании вы никогда не прочитаете данных больше чем вам может предоставит источник и не запишите в никуда в случае если приемник данных не готов в данный момент их принять. Если что-то не так то результат функция всегда вернет нуль или значение отличное от того что вы ей передали в поле Count.

На этом удобства не заканчиваются. Если вы уже умеете использовать такой гибкий механизм как исключения, то вы должны оценить методы ReadBuffer, WriteBuffer, они вызывают Read и Write для передачи данных, но в случае неудачи вызывают исключения EReadError и EWriteError соответственно. Это нам дает возможность грамотно выйти из непредвиденной ситуации и объяснить пользователю, почему его действия не увенчались успехом.

Пример: Создаем пустой текстовый файл на диске C: с именем "1.txt".

 
procedure SeadFromStream(AStream : TStream);
var
  i          : Integer;
Begin
  try
    AStream.ReadBuffer(i, sizeof(i));
  except
    on EReadError do ShowMessage('Ошибка чтения данных');
    else ShowMessage('Неизвестная ошибка ');
  end;
end;
 
procedure TForm1.FormCreate(Sender: TObject);
var
  F : TStream;
  i : Integer;
begin
  Try
    F := TFileStream.Create('C:\1.txt', fmOpenRead);
    SeadFromStream(F);
    F.Free;
  finally end;
end;

Под отладкой все равно вылетают исключения, но так бывает только под отладкой...

Кто повнимательнее заметил заметил что открытие файла тоже в защищенной секции. Что поделаешь, отстуствие файла это тоже ошибка, но тут я ее не стал отдельно отрабатывать, так как для исследуемой функции LoadFromStream не имеет значения к какому из потоков принадлежит переданный ей объект.

Уточню, что методы ReadBuffer и WriteBuffer определенны уже в TStream. Т.е. если вы вдруг захотите написать своего наследника TStream, для своего специфического источника данных, то вам не нужно будет их переопределять самому(ой).

Говоря об операциях чтения и записи следует уточнить, что не все наследники TStream позволят вам писать/читать данные одновременно. Например, в ресурс нельзя ничего писать, а из файла открытого для записи бесполезно читать. Это значит что если класс имеет метод WriteToStream, то передавать ему объект класса TResourceStream будет неправильно.

Метод Seek устанавливает позицию чтения/записи в потоке, хотя он и реализован в TStream, но его ОБЯЗАНЫ перекрывать все наследники. Он существует в 2х вариациях

 
function TStream.Seek(Offset: Longint; Origin: Word): Longint;
function TStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64;

Перекрыть нужно хотя бы один. Если один из них не будет реализован, то при необходимости всегда будет вызван другой. Если не будет ни одного, то при попытке изменить позицию чтения произойдет исключение.

Метод Seek позволяет производить установку позиции от начала потока (параметр Origin = soBeginning), от текущей позиции (Origin = soCurrent), или от конца(Origin = soEnd).

Для получения позиции от начала хранилища удобно пользовать свойством Position.

Еще один метод, который часто нужно перекрывать в наследнике это SetSize

 
procedure TStream.SetSize(NewSize: Longint);

Он хоть и не абстрактный но пустой. Его сделали пустым на случай если вдруг ваш поток не поддерживает операции записи (это допустимо). В этом случае, было бы нелогичным требовать от наследника TStream реализовывать возможность изменения размера его внутреннего хранилища данных, тогда как он не предусматривает такую операцию.

В простых случаях линейных хранилищ Функцию GetSize можно не переопределять. Она самостоятельно использует метод Seek для получения размеров хранилища.

При использовании объектов класса следует использовать публичное свойство Size.

Теперь о реализованных высокоуровневых возможностях.

Копирование больших фрагментов (более 100кб)

 
function CopyFrom(Source: TStream; Count: Int64): Int64;

Замечательная функция, она нам позволит скопировать большой блок данных из чужого потока, начиная с текущей позиции в потоке Source, при этом начнет писать данные, начиная с текущей позиции в нашем потоке. Для копирования будет выделен внутренний буфер размером в 60 кб, позволяющий сравнительно большими блоками копировать данные, при этом не занимая надолго сам источник данных (источник может оказаться неспособным к работе с несколькими процессами). И еще если в качестве Count указать 0, то функция поймет что нужно скопировать весь поток от начала (Внимание! Не от текущей позиции) до конца. Весьма удобная функция, при перекачке данных из медленного источника в быстрый или из ReadOnly потока в поток доступный для записи.

Сериализация объектов.

Сериализация это перевод структур данных (в данном случае классов наследников TComponent) в бинарное представление. В таком виде данные объектов можно передать из программы или сохранить на диске. Тут же уточняю сохраняться лишь published свойства. Потому если вы хотите использовать такой механизм в своей программе, то в published секцию следует перенести все свойства определяющие состояние объекта.

 
  function ReadComponent(Instance: TComponent): TComponent;
  procedure WriteComponent(Instance: TComponent);

Можно записывать несколько объектов подряд, сохранять картинки, тесктовые файлы и много чего другого. Кстати, таким же образом создается любая форма, только источником для нее становиться TResourceStream. Файл DFM сохраняется в ресурс, на лету конвертируется из текстового ресурса в бинарный и становиться доступным для загрузки функцией ReadComponent. Однако перед использованием этих функций нестандартные компоненты следует зарегистрировать при помощи функции RegisterClass. (Подробный пример см. в справке по Delphi "TStream.ReadComponent Method")

Дополнительные возможности.

Реализация интерфейса IStream. Связь с COM.

Наверное некоторые уже заметили созвучность имени класса TStream и COM интерфейса IStream. IStream - это универсальный интерфейс приема-передачи данных в Windows. Если бы нам удалось подружить TStream и IStream, то задачу передачи данных между двумя сторонними объектами можно было бы считать решенной. Например, передать картинку из потока TFileStream в объект класса Bitmap библиотеки GDI+. Аналогичная задача стоит при обмене данными между приложением и своей собственной Dll. Как известно в Dll нельзя передавать объекты классов, а интерфейсы очень даже можно, при этом идеально сохраняется принцип инкапсуляции класса и объектный подход к разарботке приложений.

Ближе к делу. Для того чтобы получить интерфейс IStream имея объект наследник TStream необходимо дополнительно воспользоваться классом TStreamAdapter, который и реализует IStream, но не сам, а с помощью класса наследника TStream. Вот его кноструктор.

 
constructor Create(Stream: TStream; Ownership : TStreamOwnership = soReference);

Видно, что при создании объект должен принять экземпляр класса наследника TStream, но кроме того у него есть еще такой замечательный параметр Ownership, который указывает нужно ли уничтожать сам объект потока (тот который передали ему в конструкторе Stream: TStream). Напомню, что после того как интерфейс освобождается (переменной интерфейса присваивают nil) и больше не остается ссылок на этот объект (ссылока значит то, что у этого объекта получали интерфейс), сам объект, который релизовывал интерфейс уничтожается. Уничтожается, ну и пусть, но ведь после уничтожения у нас останеться неуничтоженным сам объект потока (наследник TStream, который и реализовывал передачу данных). Уничтожить самостоятельно мы его не можем, ведь мы не знаем, когда клиент прекратит с ним работать. Вот для это и нужно в качестве Ownership передвать значение soOwned, т.е. адптер владеет экземпляром потока и удалит его, перед тем как самоуничтожиться.

Звучит это все страшно, а используется очень просто. Вот пример.

 
var
  strm : IStream; 
  mem : TMemorySteam;
Begin
  mem := TMemorySteam.Create;
  strm := TStreamAdapter.Create(mem, soOwned) as IStream;

Все теперь передаем интерфейс strm в Dll ил вообще куда угодно и не думаем о уничтожении ни TStreamAdapter ни TMemorySteam, в нужное время они сами уничтожаться, тогда когда клиент закончит с ними работать.

Теперь рассмотрим случай, когда программа на делфи является не сервером, а клиентов, т.е. ей передают интерфейс IStream, объекта потока созданного неизвестно где. Идеально было бы если бы мы получили не IStream, а TStream, ведь все объекты VCL умеют работать именно с TStream, а работать с IStream они не умеют. Решение в лоб состоит в том, чтобы банально скопировать все данные из IStream например в TMemorySteam. Однако, таким образом мы получаем лишнее копирование. Это ничего, когда данных на 20кб, другое дело если их 1Гб, тогда дополнительное копирование выйдет боком. Для таких случаев предлагаю написать свой собственный адаптер, но уже не адаптер сервера, а клиента. Класс назвал TExternStream (от слова External т.е. внешний). Заодно это будет пример написания своего класса наследника TStream.

Основная задача этого класса переадресовать все запросы интерфейсу внешнего объекта IStream. Сам интерфейс нужно передать в конструктор. В деструкторе интерфейс освобождается FSource := nil; в классе перекрыты все абстрактные методы Read, Write, виртуальный Seek, GetSize и SetSize.

 
type
  TExternStream = class(TStream)
  protected
    FSource : IStream;
    procedure SetSize(const NewSize: Int64); override;
  public
    constructor Create(Source : IStream);
    destructor Destroy; override;
    function Read(var Buffer; Count: Longint): Longint; override;
    function Write(const Buffer; Count: Longint): Longint; override;
    function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; override;
  end;
 
procedure TForm1.FormCreate(Sender: TObject);
var
  T : TStreamAdapter;
begin
  RegisterComponents
end;
 
{ TExternStream }
 
 
constructor TExternStream.Create(Source: IStream);
begin
  inherited Create;
  FSource := Source;
end;
 
destructor TExternStream.Destroy;
begin
  FSource := nil;
  inherited;
end;
 
function TExternStream.Read(var Buffer; Count: Integer): Longint;
begin
  if FSource.Read(@Buffer, Count, @Result) <> S_OK
  then
    Result := 0;
end;
 
function TExternStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64;
begin
  FSource.Seek(Offset, byte(Origin), Result);
end;
 
procedure TExternStream.SetSize(const NewSize: Int64);
begin
  FSource.SetSize(NewSize);
end;
 
function TExternStream.Write(const Buffer; Count: Integer): Longint;
begin
  if FSource.Write(@Buffer, Count, @Result) <> S_OK
  then
    Result := 0;
end;

Используется все это счастье следующим образм. Пусть в клиенсткую часть передали интерфейс strm : IStream, тогда создаем свой экземпляр класса TExternStream на базе этого strm и используем как любой другой объект наследник TStream. Напрмер вызываем Image1.Picture.Bitmap.LoadFromStream(aOut);

 
var
  aOut : TExternStream;
try
  aOut := TExternStream.Create(strm);
  Image1.Picture.Bitmap.LoadFromStream(aOut);
....
  aOut.Free; //Тут уничтожиться поток созданный в Dll, так как указатель на aStream занулиться. 

Чистого кода!

опубликовано
Добавить новыйПоиск
Pavia   2008-11-09 17:13:52
цитата:
Статья для тех кто до сих пор игнорирует этот весьма эффективный и быстрый метод обработки данных.

Не раскрыта тема.


цитата:
F.Free;
finally end;

Насколько я знаю правельнее
finally
F.Free;
end;


цитата:
TExternStream
= class(TStream)


А почему нетак TExternStream = class(TStream,IStrea
m)???
Анонимно - re:   2010-04-15 15:12:31
[quote=Pavia]
цитата:



А почему нетак TExternStream = class(TStream,IStrea
m)???


Потому что так пишутся только классы, реалищующие указанный интерфейс. А интерфейс IStream, как вы понимаете, реализован где-то в недрах Windows. Если TExternStream объявить так, то получается, что при запросе интрефейса IStream будет создаваться объекс TExternStream. Соответственно при обращении к методам ISteram будут вызываться методы TExternStream. А нам-то нужно совсем другое.
Добавить комментарий
Имя:
Веб-сайт:
Заголовок:
UBB-Код:
[b] [i] [u] [url] [quote] [code] [img] 
 
 
:angry::0:confused::cheer:B):evil::silly::dry::lol::kiss::D:pinch:
:(:shock::X:side::):P:unsure::woohoo::huh::whistle:;):s
:!::?::idea::arrow:
 
Security Image
Пожалуйста, введите проверочный код, который Вы видите на картинке.
 
< Пред.   След. >