Центральной частью нашей программы будет процедура
compareFiles
, которая сравнивает содержимое файлов, чьи
имена переданы ей как параметры, и возвращает результат сравнения. Ложное
возвращаемое значение означает совпадение, истинное — различие.
Возможен ещё один исход при сравнении двух файлов: ошибка при открытии или чтении. В этом случае процедура возвратит неопределённое значение.
Программируя процедуру compareFiles
, мы с особой
тщательностью отнесёмся к источникам возможных ошибок и их обработке.
Процедура compareFiles
пригодится нам в одной из следующих
глав.
Один из возможных подходов к сравнению файлов, наиболее простой в реализации,
состоит в следующем. Открыв два файла, прочитаем целиком всё их содержимое
в две переменные — $buffer1
и $buffer2
.
Сравним эти переменные оператором ne, который, напомним,
возвращает истинное значение для отличающихся строк, и ложное для одинаковых.
Именно это значение и следует возвратить из процедуры.
Мы обсудим этот приём хотя бы для того, чтобы научиться считывать содержимое файла в переменную целиком. Однако такой подход придётся отвергнуть с негодованием. Дело в том, что совпадение файлов — скорее исключение, чем правило. И, тем не менее, при сравнении двух файлов их содержимое копируется в память полностью, даже если файлы различаются уже в самом первом байте. Оператор ne сразу же обнаружит это различие, и не станет осуществлять теперь уже ненужное сравнение последующих байтов, но те уже заняли память, возможно, немалую. Считывание остатка содержимого файлов оказалось бесполезным. Но самый главный недостаток описанного подхода может повлиять не только на эффективность программы, но на саму возможность её работы. Файлы бывают очень большими, и могут не поместиться не только в оперативную память, но и в так называемую виртуальную, которая «живёт» на жёстком диске компьютера и выделяется программе при нехватке оперативной. Острая недостаточность и той и другой памяти приведёт к повышенной нагрузке на процессор компьютера и последующее аварийное завершение программы.
Другая крайность, не столь опасная, это последовательное считывание из каждого файла байта за байтом и сравнение этих байтов. При обнаружении различающихся байтов считывание и сравнение прекращается, и возвращается истинное значение. Прекращается оно также, если не удалось прочитать байты из обоих файлов (из-за того, что оба файла считаны полностью). В последнем случае возвращается ложное значение в знак полного совпадения файлов. При возникновении ошибки чтения в хотя бы одном файле дальнейшее чтение следует завершить и сообщить об ошибке. В любом случае открытые файлы нужно закрыть.
Недостаток этого подхода связан с тем, что операцию чтения придётся проводить для считывания каждого байта. Обычно в жизни за чтением одного байта из файла следует чтение соседних байтов. С учётом этого проектируются и устройства постоянной памяти (жёсткие диски), и операционные системы. Файловые системы как правило рассматривают файлы как последовательности не байтов, а блоков, вмещающих от полукилобайта до нескольких килобайт. Считывание одного байта означает, что с большой вероятностью последует чтение соседних байт из того же блока. Поэтому при первом обращении к байту в файле считывается весь блок, содержащий этот байт. Блок сохраняется в специально выделенной для этого памяти, называемой дисковым кэшем, и ждёт там своего часа в надежде на скорые последующие обращения к соседним байтам. Считывание соседних байтов производятся уже не с диска, а из дискового кэша, что происходит намного быстрей: не нужно считывающие головки диска перемещать в положение, соответствующее положению блока на диске. Этот приём называется кэшированием и очень широко применяется в информатике.
Хотя кэширование существенно ускоряет последовательное чтение файла, плата за каждый вызов команды чтения велика, так как операция чтения весьма сложно устроена. Вызывать её для одного байта почти так же накладно, что и для килобайта. Здесь уместна такая аналогия: отвезти одного пассажира на маршрутном такси не намного дешевле, чем сразу пятнадцать человек. Зато перевозка сразу пятнадцати человек намного выгодней, чем пятнадцать рейсов, в каждом из которых перевозится только один пассажир.
Остановимся на подходе, при котором из файлов раз за разом считываются не
отдельные байты, а порции разумного размера. В остальном алгоритм точно
такой же, что и при побайтном считывании. Какой должен быть размер этих порций
для самой быстрой работы программы, довольно трудно предсказать, однако опыт
подсказывает, что размер, сравнимый с килобайтом, даёт хорошие результаты.
При программировании процедуры compareFiles
мы
предусмотрим, кроме имён сравниваемых файлов, ещё и третий параметр — размер
порции (буфера) ввода.