Наконец-то мы получили код, который безошибочно передает строку, не имея при этом ограничений длины (кроме ограничения на длину
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 на компакт-диске) мы увидим, что проблема не исчезает, а просто становится менее заметной.
Листинг 3.41. Побочное изменение переменной после низкоуровневого копирования
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;
На экране вместо ожидаемого Hello!!! появится Good bye. Это происходит вот почему: процедура
Move
осуществляет простое побайтное копирование одной области памяти в другую, механизм изменения счетчика ссылок при этом не срабатывает. В результате менеджер памяти не будет знать, что после завершения локальной процедуры
CopyRecord
остаются ссылки на строку "Hello!!!". Память, выделенная этой строке, освобождается. Но
Rec.Str
продолжает ссылаться на эту уже освобожденную память. Для строки
S
выделяется свободная память — та самая, где раньше была строка
LocalRec.Str
. А поскольку
Rec.Str
продолжает ссылаться на эту область памяти, поэтому обращение к ней дает строку "Good bye", которая теперь там размещена.