Наконец-то мы получили код, который безошибочно передает строку, не имея при этом ограничений длины (кроме ограничения на длину AnsiString
) и не расходуя понапрасну память. Правда, сам код получился сложнее. Во-первых, из записи исключено поле типа string
, и теперь ее можно без проблем читать и писать в поток. Во-вторых, в поток после нее записывается длина строки. В-третьих, записывается сама строка.
Параметры вызова методов ReadBuffer
и WriteBuffer
для чтения/записи строки требуют дополнительного комментария. Метод WriteBuffer
пишет в поток ту область памяти, которую занимает указанный в качестве первого параметра объект. Если бы мы указали саму переменную Msg
, то записалась бы та часть памяти, которую занимает эта переменная, т. е. сам указатель. А нам не нужен указатель, нам необходима та область памяти, на которую он указывает, поэтому указатель следует разыменовать с помощью оператора ^
. Но просто взять и применить этот оператор к переменной Msg
нельзя — с точки зрения синтаксиса она не является указателем. Поэтому приходится сначала приводить ее к указателю (здесь подошел бы любой указатель, не обязательно нетипизированный). То же самое относится и к ReadBuffer
: чтобы прочитанные данные укладывались не туда, где хранится указатель на строку, а туда, где хранится сама строка, приходится прибегнуть к такой же конструкции. И обратите внимание, что прежде чем читать строку, нужно зарезервировать для нее память с помощью SetLength
.
Вместо приведения строки к указателю с последующим его разыменованием можно было бы использовать другие конструкции:
Stream.ReadBuffer(Msg[1], MsgLen);
и
Stream.WriteBuffer(Msg[1], MsgLen);
Это дает требуемый результат и даже более наглядно: действительно, при чтении и записи мы работаем с той областью памяти, которая начинается с первого символа строки, т. е. с той, где хранится сама строка. Но такой способ менее производителен из-за неявного вызова UniqueString
. В нашем случае мы и так защищены от побочных изменений других строк (при записи строка не меняется, при чтении она и так уникальна — это обеспечивает SetLength
), поэтому вполне можем обойтись без этой в данном случае излишней опеки со стороны компилятора.
Если сделать MsgLen
не независимой переменной, а полем записи, можно сэкономить на одном вызове ReadBuffer
и WriteBuffer
.
Недостатком этого метода является то, что мы вынуждены переделывать под него запись. В нашем примере это не составило проблемы, но в реальных проектах запись обычно предназначена не только для чтения и сохранения в поток, и если взять и выкинуть из нее строки, то все прочие участки кода станут более громоздкими. Поэтому в реальности приходится писать отдельные процедуры, которые сохраняют запись не как единое целое, а по отдельным полям.
Ранее мы говорили о том, что копирование записей, содержащих поля типа AnsiString
, в рамках одного процесса маскирует проблему, т. к. указатель остается допустимым и даже (какое-то время) правильным. Но сейчас с помощью приведенного в листинге 3.41 кода (пример RecordCopy на компакт-диске) мы увидим, что проблема не исчезает, а просто становится менее заметной.
type
TSomeRecord = record
SomeField: Integer;
Str: string;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Rec: TSomeRecord;
S: string;
procedure CopyRecord;
var
LocalRec: TSomeRecord;
begin
LocalRec.SomeField:= 10;
LocalRec.Str:= 'Hello!!!';
UniqueString(LocalRec.Str);
Move(LocalRec, Rec, SizeOf(TSomeRecord));
end;
begin
CopyRecord;
S:= 'Good bye';
UniqueString(S);
Label1.Caption:= Rec.Str;
Pointer(Rec.Str):= nil;
end;