Когда мы искали к нам на работу еще одного программиста на Delphi, я проводил техническую часть собеседования. Там, помимо прочего, я просил соискателя написать очень небольшую процедурку, чтобы понять, умеет, собственно, человек программировать или нет. Забегая вперед, скажу, что выяснил, что пугающе много программистов на Delphi не умеют программировать на Delphi. То есть, для именно программирования используют знания о языке Паскаль, почерпнутые на ранних курсах вуза, а возможностями Delphi пользуются исключительно как редактором и визуальной средой построения интерфейсов.

Я решил немного восполнить этот пробел в знаниях и, возможно, открыть кому-то глаза на мощные возможности Object Pascal.

Мое задание звучало примерно так:

Нужно написать процедуру, которая копирует файл из одного в другой, не используя при этом стандартную APIшную функцию CopyFile. Процедура принимает имя исходного файла и имя конечного файла. Для простоты будем считать, что исходный файл с таким именем точно есть, конечного точно нет, и его можно создать и в него записать.

Здесь я делал паузу и ждал, чтобы человек задал очевидные вопросы: а почему не использовать APIшную функцию и какие, может быть, есть дополнительные условия. Если человек эти вопросы задавал, это давало дополнительные плюсы к его приему. Мы искали, прежде всего, думающего программиста а не просто кодера. А думающий программист не бросается тут же делать задание, сформулированное так неконкретно и не без странностей.

Впрочем, если человек ничего не спрашивал, минусов это ему не добавляло тоже. Потому что он вполне вправе был думать, что тестовое задание на собеседовании может и не иметь связей с реальностью.

Честно говоря, так все и думали, видимо, потому что никто ни разу ничего не спросил :-). Тогда я немного дополнял условие сам:

Копирование вручную нужно для того, чтобы мы могли контролировать процесс передачи и, скажем, прервать его по запросу пользователя, если он будет слишком долгим.

Здесь особенно искушенные программисты должны хитро ухмыльнуться и спросить, почему же тогда не использовать не менее стандартную CopyFileEx, которая через callback-функцию сообщает о своем выполнении и тоже может быть прервана. Думаю, если бы я такое услышал, я был бы на 90% уверен, что человека мы возьмем :-). Однако, задание все равно попросил бы выполнить, объяснив, что меня, конечно, интересует не сама по себе операция копирования файла, а подход к ее реализации, а также в принципе манера человека писать код.

Собственно, разных подходов, по большому счету, есть три:

Вариант с WinAPI'шными OpenFile, ReadFile и прочими не использовал никто. Оно и понятно: это то же самое, что файловые переменные, только менее удобно в Delphi. А вот паскалевскими файловыми переменными пользовались все. Все — это именно все, до одного, без единого исключения (ну, разве только кроме тех, кто вообще отказался писать). Само по себе это неплохо, и успешное написание процедуры я расценивал как приемлемое решение. Дальше разговор шел о том, почему именно такой а не другой размер буфера выбрал программист, что будет, если файл нулевой длины и т.д....

Однако, по-настоящему я ждал другого. Вот этого:

Procedure CopyFile(InFileName,OutFileName:String);
Const
  BufSize=4096;
Var
  InFile,OutFile:TStream;
  Buffer:Array[1..BufSize] Of Byte;
  ReadBufSize:Integer;
Begin
  InFile:=Nil;
  OutFile:=Nil;
  Try
    InFile:=TFileStream.Create(InFileName,fmOpenRead);
    OutFile:=TFileStream.Create(OutFileName,fmCreate);
    Repeat
      ReadBufSize:=InFile.Read(Buffer,BufSize);
      OutFile.Write(Buffer,ReadBufSize);
    Until ReadBufSize<>BufSize;
  Finally
    InFile.Free;
    OutFile.Free;
  End;{Try}
End;{CopyFile}

По сути это мало чем отличается от открытия файловых переменных, BockRead'а и BlockWrite'а. И однажды один мой очень уважаемый коллега (Саш, когда у тебя сайт будет? Ссылку поставить некуда! :-) ) спрашивал меня, а чем, собственно, Stream'ы лучше, чем файловые переменные? Я тогда не нашел, что ответить, но теперь могу.

Есть две причины.

Первая — Stream'ы для сигнализации ошибок используют исключения вместо переменной IOResult с кодом возврата. А исключения — это более надежная парадигма программирования. Кроме того, в Delphi везде используются исключения, и если ваша процедура ведет себя как принято в среде, то вам только спасибо скажут те, кто будет ею пользоваться.

Вторая причина исходит из класса TStream. Он предоставляет абстрактный интерфейс чтения и записи для массива данных любой природы. Это значит, что тот же самый код можно использовать не только для файлов, но и для чего-то другого.

Вкусный пример. Вы создаете свой TStream, который при создании примает к себе другой TStream. Все данные, которые записываются в ваш поток, вы сжимаете каким-нибудь алгоритмом и уже пожатые записываете во внутрений TStream. Фактически, этот поток работает как пакующий фильтр (это, кстати, применение паттерна "Декоратор", из книжки, которая у вас обязательно должна быть). Так вот, этот поток можно использовать в нашей процедуре с одним единственным изменением в создании конечного файла: вместо

OutFile:=TFileStream.Create(OutFileName,fmCreate);

будет

OutFile:=TCompressingStream.Create(TFileStream.Create(OutFileName,fmCreate));

Вот, собственно, и все обснования. Однако, формат статьи требует какого-нибудь вывода. Вот, например:

Делфисты! Изучайте свою среду программирования и используйте потоки и исключения. Они надежно позаботятся о тех вещах, о которых вы даже не подозревали.

Комментарии: 14

  1. Dima

    Первый, наф :-)
    Иван, а у тебя никогда не возникало мысли покинуть нашу любимую родину и поехать жить и работать куда-нибудь Туда?
    Ты ж талантливый ;-)

  2. L_Dorrit

    А меня вы бы, наверное, взяли. :-) Правда, это не совсем моя заслуга... просто у меня была аналогичная задачка, решилась как раз с помощью файловых потоков. Только функция чуть длиннее, потому что она еще отображает состояние в TGauge... Да, и еще вопрос "почему нельзя API" мной бы тоже был задан. Правда, вот CopyFileEx мне как-то не приходилось использовать, поэтому такой вопрос не задавался бы...
    Да, смешно, но необходимость этой процедуры у меня появилась потому, что некоторые юзеры не способны слить себе новый экзешник с сервера. Я заморилась им по полчаса по телефону объяснять, как это делается. И написала СПЕЦИАЛЬНУЮ ПРОГРАММУ. У которой одна только функция: взять файло с сервера, положить в нужное место и создать ярлык на рабочем столе... Смешно, да? :-) И такое бывает.
    //———————————————————————————————————————
    // Копирование файла через потоки
    procedure TfmCopy.FileCopyEx(const OldName, NewDir: String; IfDeleteOld: Boolean);
    const BlockSize = 1024;
    var
    NewFileName: String;
    NewFile: TFileStream;
    OldFile: TFileStream;
    ElapsedSize: Integer;
    CopySize: Integer;
    begin
    // имя нового файла
    NewFileName := Trim(NewDir + '\' + ExtractFileName(OldName));
    // если надо удалить, удаляем
    if FileExists(NewFileName)
    and(IfDeleteOld) then
    DeleteFile(NewFileName);
    // поток для файла-источника
    OldFile := TFileStream.Create(OldName, fmOpenRead or fmShareDenyWrite);
    try
    // поток для файла-приемника
    NewFile := TFileStream.Create(NewFileName, fmCreate or fmShareDenyRead);
    try
    ElapsedSize := OldFile.Size - OldFile.Position;
    ggCopyProgress.Progress := 0;
    while ElapsedSize > 0 do
    begin
    if ElapsedSize

  3. L_Dorrit

    if ElapsedSize < BlockSize then
    CopySize := ElapsedSize
    else
    CopySize := BlockSize;
    NewFile.CopyFrom(OldFile, CopySize);
    ElapsedSize := OldFile.Size - OldFile.Position;
    ggCopyProgress.Progress := Round(OldFile.Position * 100 / OldFile.Size);
    Self.Update;
    Application.ProcessMessages;
    end;
    finally
    FreeAndNil(NewFile);
    end;
    finally
    FreeAndNil(OldFile);
    end;
    end;

  4. L_Dorrit

    Не, не взяли бы. Потому что надо было сначала проверить код и заменить угловые скобки, а я забыла. :-)

  5. Иван Сагалаев

    Совсем не смешно. Решение получилось эффективнее, чем поголовное обучение юзеров. Все так и должно быть.

  6. Витя

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

  7. Александр

    Скажу Вам так, дорогой коллега... Считаю себя профессиональным программистом. Волею судеб программирую на Делфи, разрабатываю интерфейсы пользователя, проектирую БД... Решать приходилось много разных задач. С Блоб полями БД общаюсь через потоки. Потоки использую для чтения/записи в файл (конечно, когда это уместно, ведь иногда приятно и TSTringListом воспользоваться). Да много где потоки использую - в общем знаю я что это такое.

    Перед тем как прочитать правильный ответ автора, я подумал, а что бы я написал на этом тестовом задании. И знаете, написал бы я BlockRead/BlockWrite. Ждёшь подвоха =) Решение приходит из глубины... когда Турбо Паскаль я загружал с дискетки и писал очередной клон Нортона Коммандера =)... А если бы я седел в кресле перед компьютером, пил бы кофе и занимался разработкой - то только потоки.

    Кажется так...

  8. 5ickn3ss

    Автор, на мой взгляд, предлогает очень странный алгоритм... не понятно зачем читать буффер в цикле...

    возможно я ошибаюсь, но вот так
    OutFile.CopyFrom(InFile, ReadBufSize.Size);
    было бы гораздо проще.

  9. Иван Сагалаев

    Я там писал:

    Копирование вручную нужно для того, чтобы мы могли контролировать процесс передачи и, скажем, прервать его по запросу пользователя, если он будет слишком долгим.

    И в любом случае, это же тестовая задача, ее условия продиктованы не реальностью, а желанием протестировать навыки.

  10. Денис Зайцев

    скажу, что выяснил, что пугающе много программистов на Delphi не умеют программировать на Delphi

    Ага. Я вот сегодня тоже собеседовал программиста на Delphi (текучка!), вот что он нам написал:

    buf  : integer
    
    Assignfile (F1, Name1);
    Assignfile (F2, Name2);
    
    Reset (F1, 1);
    Rewrite(F2);
    
    while not EOF(F1) do
    begin
        blockread(F1, buf, 2);
       blockwrite(F2, buf, 2);
    end;
    
    close(F1); close(F2);
    
  11. Viktor Vetoshkin

    Вот так и пролетают. Я пишу на Delohi(2,5) с 1996(?). Последнее время больше на Мелкосфтовском Си 6.0 и на С++ Билдере 6.0. Проекты различной тяжести - от десятка тысяч до 200 - 300 тысяч строк. Но вот навскидку написать CopyFile(Ex) / TStreamFile ( ?) TFileStream - увольте - не смогу.
    Почему:
    1. Я всегда был ( и буду) программистом, не кодером. Ничего плохого в это слово не вкладываю, просто разные профессии.
    2. Если сталкиваюсь с чем - нибудь первый раз - ищу не менее 2 - 3х вариантов, выбираю оптимальный и впоследствии стокнувшись задачей ( не оператором, а именно с нормально сформулированной задачкой) быстренько нахожу, копирую, вставляю и вперед с песней.
    3. А имея на ПК старые проекты, готов посоревноваться с любым кодером на реальном проекте.
    4. Для справки: к сожалению до сих пор тыкаю в клаву 2 ( двумя) пальцами.
    Viktor: vvv2104@yandex.ru

  12. AL-one

    А я хочу придраться к варианту решения самого автора.
    CopyFile кроме данных копирует и атрибуты файла, а CopyFileEx еще и дополнительные потоки и прочую NTFS-специфичную лабуду.
    А в задании сказано скопировать файл, а не только данные файла.
    Продолжу ворчать :)
    Мне кажется не очень красиво выделять в стеке 4 КБайта под буфер, лучше как-нибудь динамически.

  13. Mark

    Assignfile (F1, Name1);
    Assignfile (F2, Name2);

    Reset (F1, 1);
    Rewrite(F2);

    Забавно... я бы по старой памяти в этом случае начал писать {$i-},{$i+} и проверку IOResult... Хм... неужели на Делфе до сих пор так пишут? Я уж кроме как потоками и CopyFile-ом других вариантов и не вспомню сразу-то :)

  14. 2009 год

    а Ваш, Иван, метод по прежнему работает шустрее остальных =) пасиб) а главное, не грузит систему как tc или explorer, спасибо за наводку!

Добавить комментарий