15 августа 2011 г.

Задачка №12

Что не так с этим кодом?

Сегодня - простая задачка на знание справки Delphi. Итак, есть такой код:
var
  Buf: RawByteString;  // AnsiString для старых Delphi
  Data: RawByteString; 
  FS: TFileStream;
begin
  ...
  Data := '';
  SetLength(Buf, 10240);
  while FS.Position < FS.Size do
  begin
    // SetLength сработает для последнего куска
    SetLength(Buf, FS.Read(Pointer(Buf)^, Length(Buf))); 
    Data := Data + Buf;
  end;
end;
Этот простой код читает файл, загружая его в Data поблочно: блоками по 10 Кб, читая каждый блок в Buf и соединяя их друг за другом в цельное содержимое.

Однако в этом коде есть одна проблема. Что с ним не так? И как это исправить?

Ответ.

14 комментариев:

  1. Строки в Delphi используют Copy On Write, а из-за прямого доступа на запись к данным строки этот механизм не работает. UniqueString поможет.

    ОтветитьУдалить
  2. Или же в FS.Read использовать Buf[1] вместо Pointer(Buf)^.

    ОтветитьУдалить
  3. AnsiString = array[0.. ] of Char
    байт с адресом 0 в этом массиве указывает длину строки. Загрузка текстовой строки Pointer(Buf)^ приведет к тому что в [0] будет записано что угодно, но только не длина. Соответственно операция Data := Data + Buf в итоге даст мусор.

    ОтветитьУдалить
  4. такое тоже помогает:
    Data := Data + Copy(Buf, 1, Length(Buf));

    но совет с Buf[1] правильнее и проще :)

    ОтветитьУдалить
  5. Однако код-то работает.

    ОтветитьУдалить
  6. Забавные комментарии.
    Вот Вы подумайте, как в один байт уместить число 10240 (длина строки в примере)? Никак.
    Переменные типа string (AnsiString/RawByteString) указывают на начало строки, а не на байт, содержащий размер строки (как в старом-добром Pascal'е, или как в случае с типом ShortString). Поэтому в примере всё корректно в этом плане.

    Мне кажется, что проблема заключается в том, что FS.Read в случае ошибки чтения вернёт 0 (фактическое кол-во прочитанных байт), что приведёт к... бесконечному циклу.
    Вероятность возникновения такой ошибки - не велика, и потому этот код работает. Но если есть вероятность выше ноля, то такое когда-нибудь да и произойдёт. По закону Мёрфи :)
    Исправить код можно добавлением условия в while:
    while (FS.Position < FS.Size) and (Length(Buf) > 0) do

    Но лично я предпочитаю немного другой, более прозрачный стиль кодирования:
    while FS.Position < FS.Size do
    begin
    Count := FS.Read(Pointer(Buf)^, Length(Buf));
    if Count = 0 then
    //попытаться понять, в чём ошибка чтения, или просто передать ошибку вверх:
    RaiseLastOSError;
    if Count < Length(Buf) then
    SetLength(Buf, Count);
    Data := Data + Buf; // это тоже не очень хорошая строка - за кадром происходит перераспределение памяти
    end;

    ОтветитьУдалить
  7. Chaa прав. В строке 14 нужно добавить
    UniqueString(AnsiString(Data));
    Ну или если оптимальнее:
    if Data = Buf then
    UniqueString(AnsiString(Data));

    ОтветитьУдалить
  8. Действительно, UniqueString тут не хватает.. это интересно. Получается так, что первая порция данных потеряется (вместо неё будет дублироваться вторая порция данных).

    Как оно будет оптимальнее - здесь нельзя сказать однозначно, т.к. нам неизвестна сама задача. Можно вообще весь файл считал одним вызовом FS.Read(... FS.Size - FS.Position). Или переменную Buf объявить как указатель и вручную выделять/освобождать под него память, плюс под строку Data сначала выделить память вызовом SetLength(Data, FS.Size - FS.Position) перед циклом, а потом в цикле копировать порцию из Buf.

    ОтветитьУдалить
  9. >Действительно, UniqueString тут не хватает.. это интересно

    При использовании в FS.Read Buf[1] вместо Pointer(Buf)^ вызов UniqueString генерируется компилятором.

    ОтветитьУдалить
  10. Конструкция вида Buf[1] хорошо работает и во многих других случаях. А вот интересно, есть ли у нее недостатки или ее можно слепо лепить всюду, где она применима?

    ОтветитьУдалить
  11. Есть небольшой недостаток, но об этом - в ответе... :)

    ОтветитьУдалить
  12. У меня попутный вопрос. Если такая "засада" со строками, то что будет при использовании динамическим массивов (type tarr = array of Integer)? Какие "подводные камни"?

    ОтветитьУдалить
  13. Выглядит адекватно. Еще бы вначале
    FS.Position := 0;

    ОтветитьУдалить
  14. На первый взгляд, UniqueString() не нужен, т.к. SetLength() гарантирует уникальность строки. Однако функция, реализующая конкатенацию, в случае пустого первого аргумента вызывает функцию присваивания и получается, что Data указывает на то же, что и Buf. Т.е., по сути, вызов UniqueString() нужен ровно один раз после конкатенации. Причём, хоть UniqueString(Buf), хоть UniqueString(Data).

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.