Когда мы искали к нам на работу еще одного программиста на Delphi, я проводил техническую часть собеседования. Там, помимо прочего, я просил соискателя написать очень небольшую процедурку, чтобы понять, умеет, собственно, человек программировать или нет. Забегая вперед, скажу, что выяснил, что пугающе много программистов на Delphi не умеют программировать на Delphi. То есть, для именно программирования используют знания о языке Паскаль, почерпнутые на ранних курсах вуза, а возможностями Delphi пользуются исключительно как редактором и визуальной средой построения интерфейсов.
Я решил немного восполнить этот пробел в знаниях и, возможно, открыть кому-то глаза на мощные возможности Object Pascal.
Мое задание звучало примерно так:
Нужно написать процедуру, которая копирует файл из одного в другой, не используя при этом стандартную APIшную функцию CopyFile. Процедура принимает имя исходного файла и имя конечного файла. Для простоты будем считать, что исходный файл с таким именем точно есть, конечного точно нет, и его можно создать и в него записать.
Здесь я делал паузу и ждал, чтобы человек задал очевидные вопросы: а почему не использовать APIшную функцию и какие, может быть, есть дополнительные условия. Если человек эти вопросы задавал, это давало дополнительные плюсы к его приему. Мы искали, прежде всего, думающего программиста а не просто кодера. А думающий программист не бросается тут же делать задание, сформулированное так неконкретно и не без странностей.
Впрочем, если человек ничего не спрашивал, минусов это ему не добавляло тоже. Потому что он вполне вправе был думать, что тестовое задание на собеседовании может и не иметь связей с реальностью.
Честно говоря, так все и думали, видимо, потому что никто ни разу ничего не спросил :-). Тогда я немного дополнял условие сам:
Копирование вручную нужно для того, чтобы мы могли контролировать процесс передачи и, скажем, прервать его по запросу пользователя, если он будет слишком долгим.
Здесь особенно искушенные программисты должны хитро ухмыльнуться и спросить, почему же тогда не использовать не менее стандартную CopyFileEx, которая через callback-функцию сообщает о своем выполнении и тоже может быть прервана. Думаю, если бы я такое услышал, я был бы на 90% уверен, что человека мы возьмем :-). Однако, задание все равно попросил бы выполнить, объяснив, что меня, конечно, интересует не сама по себе операция копирования файла, а подход к ее реализации, а также в принципе манера человека писать код.
Собственно, разных подходов, по большому счету, есть три:
- паскалевские файловые переменные, BlockRead, BlockWrite
- функции WinAPI для доступа к файлам через хэндлы
- Delphi'йские потоки (TStream)
Вариант с 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
Первый, наф :-)
Иван, а у тебя никогда не возникало мысли покинуть нашу любимую родину и поехать жить и работать куда-нибудь Туда?
Ты ж талантливый ;-)
А меня вы бы, наверное, взяли. :-) Правда, это не совсем моя заслуга... просто у меня была аналогичная задачка, решилась как раз с помощью файловых потоков. Только функция чуть длиннее, потому что она еще отображает состояние в 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 >