Урок 77 - Использование потоков данных (часть 1/3)
Чтобы легче себе представить что такое поток можно представить себе магнитную ленту. Чтобы прочитать что-то с нее мы опускаем считывающую головку и читаем данные определенного объема или пока не дойдем до конца ленты. Тоже самое при записи. Если запись производить несколько раз (блок за блоком), то информация будет ложиться на ленту последовательно блок за блоком. После записи или чтения позиция считывающей головки останется там же где она осталось после предыдущей операции. Вот и все ограничения! Внутри это может быть реализовано совершенно по разному. В перерывах между чтением данных данных хранилище может шириться (например подгружаться из интернета или из диска), можно сделать бесконечную ленту, т.е. при переходе в конец потока подставить ему начало хранилища, можно реализовать принцип конвейера, т.е. хранилище маленькое, но в него постоянно подкладывают в конец (очередь), а забирают всегда поочереди. Это весьма удобный механизм при работе с одиночными объектами, коллекциями, массивами объектов и даже деревьями.
Все начинается именно с 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 соответственно. Это нам дает возможность грамотно выйти из непредвиденной ситуации и объяснить пользователю, почему его действия не увенчались успехом.
Удачи!
Встретимся в следующем уроке!