Полет над строкой
Полет над строкой
Полет над строкой Автор: Владимир Волосенков Не сильно преувеличу, если скажу, что строки являются одной из основных жемчужин языка Object Pascal. Простые и незаметные, но в то же время такие мощные. Речь, конечно, о длинных строках, объявляемых с помощью типа string (AnsiString). Строки настолько удобны, что их использование часто выходит далеко за рамки работы с текстом. Ну разве не может понравиться возможность склеить два бинарных куска данных простым сложением s1 + s2. В немалой степени таким возможностям способствует поистине гениальное устройство длинной строки. В частности она позволяет хранить любые символы, в том числе нулевые. Замечу, что во многих языках строки представляют собой указатели на первый символ строки, которая должна заканчиваться нулем. Такие строки называются zero-terminated strings. Недостатки такого подхода очевидны: невозможность хранить нулевые символы в строке, медленная работа из-за необходимости сканирования строки для нахождения ее длины, блуждание и порча памяти в случае отсутствия завершающего нуля. Паскалевская строка вместо этого хранит свою длину в виде четырехбайтного значения по отрицательному смещению от начала самой строки. Но при этом еще автоматически подставляет нулевой символ в конце строки! То есть такая строка может выступать и в качестве zero-terminated string с помощью простого приведения типа к указателю на строку PChar. Это часто требуется, например, при вызове функций WinAPI. Возможность хранить в строке любые бинарные данные, гибкость работы с ней и широта использования невольно наводят на мысль использовать строку в качестве хранилища данных. И разработчики Delphi 6 даже включили в стандартный модуль Classes новый потоковый класс - TStringStream. Он весьма похож на TMemoryString, только внутри в качестве хранилища данных потока используется строка. Интерфейс этого класса позволяет легко заполнить поток данными из строки, произвести над ними какие-то операции, и получить обратно данные опять же в виде строки: var ss: TStringStream; s: string; i: integer; r: TRecord; begin s:= ?12345?; i:= $FFFFFFFF; ss:= TStringStream.Create( s ); try ss.Position:= 3; ss.WriteString( ?321? ); // теперь строка имеет вид ?123321? ss.Write( i, sizeof( i ) ); // теперь в конце еще добавлены 4 символа с кодом FF ss.Write( rr, sizeof( rr ); // теперь дописали еще данные некой записи (record) s:= ss.DataString; // получаем результирующую строку finally FreeAndNil( ss ); end; Такие вещи и раньше делались с помощью TMemoryString, но теперь это гораздо удобнее из-за отсутствия необходимости приведения типов. Код стал короче и прозрачнее. Но все же есть в классе TStringStream и парочка своих ложек дегтя. Во-первых, довольно странная реализация метода Write, которая не позволяет этому классу быть равнозначной заменой другим наследникам TStream. Дело в том, что данный метод всегда устанавливает размер потока по концу только что записанных данных. Так что имеет смысл только последовательная запись. Попытка переместиться внутри потока и записать небольшой фрагмент данных приведет к "обрезанию" потока по концу этого фрагмента. Во-вторых, далеко не оптимальная реализация механизмов работы с памятью (в частности интенсивное использование функции Length для определения размера потока), что делает класс несколько "тормозным" при активной работе. Такая ситуация сподвигла меня на написание своего аналога TStringStream, лишенного этих недостатков. Его вам и представляю: unit StrStrm; interface uses Classes; type TStrStream = class(TStream) private FDataString: string; FPosition: integer; FSize: integer; FMemory: pointer; protected procedure SetSize(NewSize: Longint); override; public constructor Create(const AString: string); function Read(var Buffer; Count: Longint): Longint; override; function ReadString(Count: Longint): string; function Seek(Offset: Longint; Origin: Word): Longint; override; function Write(const Buffer; Count: Longint): Longint; override; procedure WriteString(const AString: string); property DataString: string read FDataString; end; implementation { TStrStream } constructor TStrStream.Create(const AString: string); begin inherited Create; WriteString(AString);; FPosition:= 0; end; function TStrStream.Read(var Buffer; Count: Longint): Longint; begin Result := FSize - FPosition;; if Result > Count then Result := Count; Move((PChar(longword(FMemory) + longword(FPosition)))^, Buffer, Result); Inc(FPosition, Result); end; function TStrStream.Write(const Buffer; Count: Longint): Longint; begin Result := Count; if FPosition + Result > FSize then begin SetLength(FDataString, (FPosition + Result)); FSize:= FPosition + Result; if FSize > 0 then FMemory:= @(FDataString[1]); end; Move(Buffer, (PChar(longword(FMemory) + longword(FPosition)))^, Result); Inc(FPosition, Result); end; function TStrStream.Seek(Offset: Longint; Origin: Word): Longint; begin case Origin of soFromBeginning: FPosition := Offset; soFromCurrent: FPosition := FPosition + Offset; soFromEnd: FPosition := FSize - Offset; end; if FPosition > FSize then FPosition := FSize else if FPosition < 0 then FPosition := 0; Result := FPosition; end; function TStrStream.ReadString(Count: Longint): string; var Len: Integer; begin Len := FSize - FPosition; if Len > Count then Len := Count; SetString(Result, PChar(longword(FMemory) + longword(FPosition)), Len); Inc(FPosition, Len); end; procedure TStrStream.WriteString(const AString: string); begin Write(PChar(AString)^, Length(AString)); end; procedure TStrStream.SetSize(NewSize: Longint); begin if NewSize <> FSize then begin SetLength(FDataString, NewSize); FSize:= NewSize; if FSize > 0 then FMemory:= @(FDataString[1]) else FMemory:= nil; if FPosition > NewSize then FPosition := NewSize; end; end. Быстрота и удобство этого класса привели к тому, что я использую его практически везде, где раньше требовался TMemoryStream. Если к нему еще добавить методы LoadFrom... и SaveTo..., то замена будет полной. Но эту доработку я оставлю для вашего удовольствия :)