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

Начнем с того, что разберемся, что это за ошибка. Вот код:

open my $fh, '<', $file
  || die "Can't open '$file': $!";

На первый взгляд вроде нормально. Он использует распространенную идиому «откройся или умри». Он использует современный подход использования лексического дескриптора файла. Он даже использует версию «open()» с тремя аргументами. Подобный код появлялся в огромном количестве программ на Perl в течение многих лет. В чем может быть проблема?

Я дам вам пару минут, чтобы вы внимательно посмотрели и решили, в чем, по вашему мнению, проблема.

[ … Время проходит … ]

Так что ты думаешь? Вы видите, в чем проблема?

Проблема в том, что нет проверки ошибок.

— Что ты имеешь в виду, Дэйв? Я слышу, как ты говоришь. «Там есть проверка ошибок — я это ясно вижу». Некоторые из вас, возможно, даже задаются вопросом, не схожу ли я с ума.

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

$ perl -e'open my $fh, "<", "not.there" || die $!'
$

Вы ожидаете увидеть там сообщение «умереть». Но не появляется. Ладно, возможно, я вру. Возможно, у меня действительно есть файл с именем «not.there». Давайте попробуем другую, немного другую версию кода.

$ perl -e'open(my $fh, "<", "not.there") || die $!'
No such file or directory at -e line 1.

И там мы видим сообщение об ошибке. Этого файла действительно не существует.

Так что же пошло не так с первой версией? Конечно, хороший способ начать это делать — сравнить две версии и посмотреть на различия между ними. Разница здесь в том, что когда я заключал в скобки параметры «open()», он начинал работать. И когда вы что-то исправляете, добавляя скобки, можно с уверенностью сказать, что проблема сводится к приоритету.

Порядок старшинства операторов Perl указан в perldoc perlop. Если вы посмотрите на этот список, то увидите, что использованный нами оператор или (||) находится на 16-й позиции в списке. Но какие еще операторы мы используем в нашем коде? Ответ скрывается на 21 позиции в списке. Когда мы вызываем встроенную функцию Perl без использования круглых скобок вокруг параметров, это называется оператором списка. И операторы списка имеют довольно низкий приоритет.

Все это означает, что наш исходный код на самом деле анализируется так, как если бы мы написали его так:

open my $fh, '<', ($file
  || die "Can't open '$file': $!");

Обратите внимание на круглые скобки вокруг $file и (что особенно важно) на весь пункт «или умереть». Это означает, что выражение в квадратных скобках оценивается и передается в «open()» в качестве третьего аргумента. И когда Perl вычисляет это выражение, он делает хитрую штуку с «булевым коротким замыканием». Выражение «А || B» сначала оценивает A и, если это правда, возвращает его. Только если A ложно, он продолжит оценку B и вернет это. В нашем случае имя файла всегда будет истинным (ну, если у вас нет файла с именем «0»), поэтому вторая половина выражения (бит «или умереть…») никогда не оценивается и фактически игнорируется.

Вот почему я сказал в самом начале, что в этом коде вообще нет проверки ошибок — это буквально правда, поскольку проверка ошибок вообще не имеет никакого эффекта.

Итак, как мы это исправим? Ну, мы уже видели один подход — вы можете явно добавить круглые скобки вокруг аргументов «open()». Но Perl-программисты не любят использовать ненужные знаки препинания, и я уверен, что видел, как это написано без круглых скобок, так как же это работает?

Если вы еще раз взглянете на таблицу приоритетов операторов и посмотрите вниз под операторами списка, вы увидите еще один оператор «или» (тот, который на самом деле является словом «или», а не знаком препинания). Он находится в самом низу списка — на позиции 24. А это значит, что мы можем использовать эту версию без круглых скобок вокруг параметров «open()».

open my $fh, '<', $file
  or die "Can't open '$file': $!";

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

Хуже всего в этом баге то, что он появляется в самый неподходящий момент. Если ваш файл существует и вы можете его успешно открыть, то все работает нормально. Что-то идет не так, только когда… ну, когда что-то идет не так. Если вы не можете открыть свой файл по какой-либо причине, вы не узнаете об этом. Что плохо.

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

use strict;
use warnings;
use Test::More;
use Test::Exception;

dies_ok {
  open my $fh, '<', 'not.there'
    || die $!;
};

done_testing;

И каждый раз это терпело бы неудачу. Но если вы переключились на другой оператор «или», то это сработает.

Есть еще один подход, который вы можете использовать. Вы можете использовать autodie в своем коде и просто забыть о добавлении or die к любому из ваших вызовов open().

use autodie 'open';

# Dies if the open fails
open my $fh, '<', $file;

Эту ошибку легко внедрить в ваш код, но ее сложно отследить. Кто уверен, что его нет ни в одном из их кодов?

Первоначально опубликовано на Perl Hacks.