Подавление ошибок - теория и практика

Начну с пространного вступления.

Мир PHP очень богат на мифы. Начинающие (и, к сожалению, не только начинающие) программисты зачастую не могут понять истинные причины странного, по их мнению, поведения, не умеют правильно тестировать код, не умеют учиться и изучать. Как следствие, периодически возникают ошибочные представления о работе PHP, которые первым делом попадают туда, куда им никак нельзя попадать — в комментарии к официальной документации PHP. Второе направление атаки — персональные блоги, форумы, статьи — как правило, менее опасное, поскольку не приводит к таким последствиям.

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


  • составление массива строк и последующий implode() быстрее, чем последовательная конкатенация (автор некорректно написал тест производительности);

  • оператор подавления ошибок очень здорово смотрится перед include (автор плохо понимал, к чему это приводит);

  • тот же оператор работает ужасно медленно, и лучше его вообще не использовать (по всей видимости, опять виноваты плохой тест производительности и плохое понимание тоже).

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

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

Все мы знаем, что код на PHP иногда порождает ошибки. В одних случаях эти ошибки допускает сам программист, в других случаях ошибки проявляются в нормальном коде как следствие сбойного окружения или некорректной настройки интерпретатора. Некоторые ошибки можно перехватить и обработать, другие нет. Разумеется, в контексте подавления мы можем говорить только о перехватываемых ошибках.Итак, если мы ставим "собаку" перед каким-либо выражением, в операционном массиве виртуальной машины появляются две новые инструкции: BEGIN_SILENCE и END_SILENCE. Код, ошибки которого мы хотим подавить, находится между этими двумя инструкциями и никак не отличается от кода без этих инструкций (для краткости я буду называть этот код "тихим"). Отсюда следует первый важный вывод, который подтверждается тестами: оператор подавления ошибок не замедляет выполнение "тихого" кода. Мнение, что "собака" чутко следит за вверенным ей кодом (потребляя процессорное время) и подавляет ошибки при их возникновении, — ошибочно, работает она принципиально иначе.

Если говорить о содержании инструкций оператора подавления, то делают они одну единственную вещь — устанавливают текущий уровень перехвата ошибок. Соответственно, BEGIN_SILENCE устанавливает этот уровень в ноль (никакие ошибки не перехватываются), а END_SILENCE восстанавливает снятый первой инструкцией уровень обратно. Важно отметить, что в пределах "тихого" кода можно вызвать функцию error_reporting() для установки нового уровня перехвата, либо сделать то же самое с помощью функции ini_set(). И поведение "тихого" кода изменится: он перестанет быть "тихим". Здесь мы можем поинтересоваться, что же будет делать инструкция END_SILENCE, если уровень перехвата был изменён в "тихом" коде. Логика её работы достаточно проста: если текущий уровень равен нулю, восстанавливается тот уровень, который был до выполнения инструкции BEGIN_SILENCE (даже если внутри "тихого" кода устанавливался другой уровень, а потом был сброшен, он не сохранится по выходу из "тихого" кода). Если же уровень остался ненулевой, предыдущий уровень восстановлен не будет. В любом случае, я бы не рекомендовал вручную менять уровень перехвата в разных частях кода, но об этом чуть ниже.

Как быстро работает сам оператор подавления ошибок? По-моему, это вопрос риторический. Две инструкции операционного массива, написанные на C и достаточно простые по своей логике, тормозить не могут. В совокупности с отсутствием задержек при выполнении "тихого" кода мы определяем, что "собака" вовсе не медленная, и с точки зрения производительности бояться её не стоит.

Но скорость выполнения это не единственный критерий выбора. Здесь мы переходим к практикам использования, но для начала разберёмся с теорией. Использование оператора подавления ошибок напрямую связано с Вашей идеологией обработки ошибок. Лично моя идеология весьма проста: любые ошибки всех уровней должны быть перехвачены и обработаны (например, залогированы). Сейчас я не буду углубляться в вопросы наладки механизма обработки ошибок, коротко скажу лишь, что всегда, когда это возможно, я использую собственный обработчик ошибок (устанавливаемый через set_error_handler()), в который попадают ошибки всех перехватываемых типов (уровень перехвата установлен в /-1/ — для всех существующих и будущих уровней) изо всех точек кода. Самый простой способ не умереть под шквалом ошибок в таком случае — писать безошибочный код. Преимуществ у такого подхода масса, и даже с точки зрения производительности это приносит свои плоды: интерпретатор не тратит время на порождение ошибок. Однако, необходимо помнить при установке собственного обработчика ошибок, что он будет запущен и при подавленных ошибках тоже. Поэтому в коде обработчика имеет смысл прерывать обработку, если текущий уровень перехвата равен нулю. Ошибки я с помощью обработчика обычно преобразую в исключения ErrorException, что позволяет вручную перехватывать такие ошибки в коде, либо просто прерывать выполнение кода с помощью собственного обработчика исключений (предварительно залогировав само исключение). В результате мы имеем достаточно "хрупкую", но свободную от ошибок систему.Теперь осталось понять, нужны ли в коде этой системы "собаки". Для этого перечислю несложные правила, которые я посчитал наиболее корректными при работе с оператором подавления.

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

Второе правило. Если есть возможность установить оператор подавления на более низких уровнях кода, нужно это делать. Я даже больше скажу: крайне не рекомендуется ставить "собаку" перед вызовом собственных функций или при создании объектов. Во-первых, код функции может быть не рассчитан на то, что его ошибки будут подавляться. Например, конструкторы объектов часто инициализируют ресурсы или читают данные из внешних источников. Если эти методы сами не будут проверять результат своей работы, Вы можете получить неполноценный объект, дальнейшая работа с которым будет порождать новые ошибки (в том числе фатальные). Даже если сейчас установка "собаки" перед вызовом функции кажется Вам логичной, через полгода, модифицируя эту функцию, Вы уже не вспомните, как она применяется. Во-вторых, как следует из первого правила, устанавливая "собаку" в коде, Вы должны чётко понимать, против каких ошибок она устанавливается, следовательно, Вы можете установить её в более подходящем месте — на более глубоких уровнях кода. В-третьих, Вы можете подавлять ошибки функции в одном месте, но забыть их подавить в другом, где эта же функция будет вызываться. В итоге, лично я взял себе за правило ставить оператор подавления только перед системными функциями или конструкциями, тщательно отслеживая, не попадает ли под действие оператора какой-нибудь самописный метод. Помните это правило и при вызове нескольких функций последовательно в одном выражении — не ставьте оператор подавления перед всем выражением, определите потенциально опасный вызов и поставьте "собаку" перед ним. И да: забудьте про trigger_error(), исключения — наше всё.

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

Четвёртое правило. Если Вы не уверены, в каком окружении будет выполняться Ваш код (к примеру, если Вы пишете публичную библиотеку), старайтесь не использовать "собаку" вовсе. Проблема в том, что пользователь Вашей библиотеки может использовать иную модель обработки ошибок, и его обработчик ошибок будет работать не так, как Вы предполагаете. Если без оператора подавления Вам обойтись трудно, приложите к коду библиотеки свой обработчик ошибок и явно задокументируйте необходимость его использования.

Я перечислил четыре основных правила, ограничивающих применение оператора подавления ошибок в PHP, теперь мы можем рассмотреть ситуации, когда этот оператор можно (и рекомендуется) применять.

Если Вы знаете, что нужный элемент данных (например, ключ массива или переменная) может по каким-то причинам отсутствовать, и это некритично, поскольку Вы можете использовать вместо него значение /null/, — не стесняйтесь, ставьте "собаку" перед обращением к этому элементу. Это короче и понятнее, чем предварительные проверки. Если Вы работаете с файлами, сокетами или другими ресурсами, которые склонны порождать ошибки, и при этом всегда проверяете результат выполнения этих функций, — Вы можете подавлять их ошибки, если Вам неинтересно, что же произошло (так тоже бывает), а вбрасывать вместо этого собственное исключение. В чрезвычайных случаях, когда Вам необходимо в принципе подавить все ошибки, Вы можете их подавить, даже если это идёт в разрез с упомянутыми выше правилами. Например, нельзя вбрасывать исключения в установленном обработчике исключений и нельзя допускать ошибки в установленном обработчике ошибок. Но если Ваш обработчик логирует ошибки, а сам процесс логирования (с помощью функции error_log()) сорвётся, Вы получите такую неприятную ситуацию, и в этом случае допустимо подавить ошибки при вызове error_log(), даже если Вы не знаете причин сбоя.

Как можно заметить, приведённые ситуации встречаются не так уж и часто — но всё же встречаются. И в подобных случаях, если Вы хорошо понимаете, что делаете, оператор подавления ошибок может существенно упростить Ваш код и ускорить его выполнение без повышения каких-либо рисков. Следовательно, Вы можете использовать эту не столь критичную, но полезную возможность языка в противовес тем, кто до сих пор этого не делает. Выбор за Вами!

Записи