Perl - статьи

       

Выбрасывайте исключение вместо возврата специальных значений или установки флагов


Возврат специального значения, сигнализирующего ошибку или установка специального флага — это очень широко используемая техника обработки ошибок. Вообще говоря, это принцип сигнализирования об ошибках практически всех встроенных функции Perl. Например, встроенные функции eval, exec, flock, open, print, stat, и system — все возвращают специальные значения в случае ошибки. Некоторые при этом также устанавливают флаг. К сожалению, это не всегда один и тот же флаг. С неутешительными подробностями можно ознакомиться на странице perlfunc.

Кроме проблем последовательности, оповещение об ошибках при помощи флагов и возвращаемых значений имеет ещё один серьёзный порок: разработчики могут тихо игнорировать флаги и возвращаемые значения. И игнорирование их не требует абсолютно никаких усилий со стороны программиста. НА самом деле в void-контексте, игнорирование возвращаемых значений — это поведение Perl-программ по умолчанию. Игнорирование флага ошибки также элементарно просто — Вы просто не проверяете соответствующую переменную.

Кроме того, игнорирование возвращаемого значения в void-контексте происходит незаметно, ведь нет никак синтаксических зацепок, позволяющих это контролировать. Нет возможности посмотреть на программу и сразу увидеть "вот здесь возвращаемое значение игнорируется!". А это означает, что возвращаемое значение может быть запросто проигнорировано случайно.

Резюме: по умыслу или недосмотру программиста индикаторы ошибок часто игнорируются. Это не есть хороший подход к программированию.

Игнорирование индикаторов ошибок часто приводит к тому, что ошибки распространяют своё влияние в непредсказуемом направлении. Например:

# Найти и открыть файл с заданным именем, # возвратить описатель файла или undef в случае неудачи.... sub locate_and_open { my ($filename) = @_;

# Проверить директории поиска по порядку... for my $dir (@DATA_DIRS) { my $path = "$dir/$filename";

# Если файл существует в дикектории, откроем его и возвратим описатель... if (-r $path) { open my $fh, '<', $path; return $fh; } }


# Ошибка если все возможные локации файла проверены и файл не найден... return; }

# Загрузить содержимое файла до первого маркера <DATA/> ... sub load_header_from { my ($fh) = @_;

# Использовать тег DATA в качестве конца "строки"... local $/ = '<DATA/>';

# Прочитать до конца "строки"... return <$fh>; }



# и позже... for my $filename (@source_files) { my $fh = locate_and_open($filename); my $head = load_header_from($fh); print $head; }

Функция locate_and_open() предполагает что вызов open успешно отработал, немедленно возвращая описатель файла ($fh), какой бы ни был возвращаемый результат open. Предположительно тот, кто вызывает locate_and_open() проверит возвращаемое ею значение на предмет корректного описателя файла.

Однако, этот кто-то не делает такой проверки. Вместо тестирования на неуспех, основной цикл for

принимает "ошибочное" значение и немедленно его использует в следующих операндах. В результате этого вызов loader_header_from() передаёт это неверное значение дальше.В результате функция, которая пытается использовать это ошибочное значение, вызывает крах программы.:

readline() on unopened filehandle at demo.pl line 28.

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

Конечно, Вы можете возразить, что вина непосредственно лежит на том, кто писал тот цикл и не проверял возвращаемое значение locate_and_open(). В узком смысле это чистая правда, но глубинная вина всё-таки лежит на том, кто написал locate_and_open(), или по крайней мере на том, кто полагал, что вызывающая сторона будет всегда проверять возвращаемое значение этой функции.

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



Вот почему так мало людей заботится о проверке для операторов print:

if (!print 'Введите Ваше имя: ') { print {*STDLOG} warning => 'Терминал отвалился!' }

Такова натура человека: "доверяй и не проверяй".

Причина того, что возвращение индикатора ошибки не является лучшей практикой — в природе человека. Ошибки являются (как это предполагается) необычными случаями и маркеры ошибок почти никогда не будут возвращаться. Эти нудные и несуразные проверки почти никогда не делают ничего полезного, так что ими просто тихо пренебрегают. В конце концов, если опустить эти проверки, почти всё работает так же хорошо. Так что гораздо проще не париться с ними. В особенности когда их игнорирование является поведением по умолчанию (вспомните void-контекст)!

Не возвращайте специальные значения, когда что-то идёт не так; вместо этого выбрасывайте исключения. Огромное преимущество исключений в том, что они как бы выворачивают наизнанку обычное поведение по-умолчанию, немедленно обращая внимание на необработанные ошибки. С другой стороны, игнорирование исключений требует намеренных и видимых усилий: вы должны предусмотреть явный блок eval

для их перехвата.

Функция locate_and_open() будет намного понятнее и надёжнее, если в случае ошибок мы выбрасываем исключения:

# Find and open a file by name, returning the filehandle # or throwing an exception on failure... sub locate_and_open { my ($filename) = @_;

# Проверить директории поиска по порядку... for my $dir (@DATA_DIRS) { my $path = "$dir/$filename";

# Если файл существует в дикектории, откроем его и возвратим описатель... if (-r $path) { open my $fh, '<', $path or croak( "Located $filename at $path, but could not open"); return $fh; } }

# Ошибка если все возможные локации файла проверены и файл не найден... croak( "Could not locate $filename" ); }

# and later... for my $filename (@source_files) { my $fh = locate_and_open($filename); my $head = load_header_from($fh); print $head; }



Заметьте, что основной цикл for вообще не изменился. Разработчик, использующий locate_and_open() всё так же предполагает, что всё отработает без сбоев. Теперь этому есть обоснование, поскольку если действительно что-то пойдёт не так, выброшенное исключение автоматически завершит цикл.

Исключения — это более удачный выбор даже если даже Вы настолько дисциплинированы, что проверяете каждое выходное значение на предмет наличия ошибки:

SOURCE_FILE: for my $filename (@source_files) { my $fh = locate_and_open($filename); next SOURCE_FILE if !defined $fh; my $head = load_header_from($fh); next SOURCE_FILE if !defined $head; print $head; }

Постоянные проверки возвращаемых значений вносят посторонний шум в Ваш код в виде проверочных операторов, часто сильно ухудшая читабельность. Исключения, наоборот, дают возможность реализовать алгоритм вообще без вкраплений кода для проверки ошибок. Мы можете вынести обработку ошибок за пределы алгоритма и передать эти функции обрамляющему eval или вообще обойтись без этой обработки:

for my $filename (@directory_path) {

# Просто игнорировать файлы, которые не загружаются... eval { my $fh = locate_and_open($filename); my $head = load_header_from($fh); print $head; } }


Содержание раздела