Чего такого уникального и полезного несет обработка исключений? По сути, ничего. Все что оно позволяет сделать, можно реализовать и старыми методами. Другое дело, что исключения позволяют делать это гораздо быстрее и элегантнее.
Сначала кратко рассмотрим технические аспекты, которые вы можете более подробно узнать из документации. Классический пример:
try {
if (!mysql_connect('xxx', 'xxx', 'xxx')) {
throw new Exception('Не коннектиццо');
}
if (!mysql_select_db('xxx')) {
throw new Exception('Законектились, но не нашли БД');
}
} catch (Exception $e) {
print $e->getMessage();
exit();
}
mysql_query('…');
В примере осуществляется подключение к базе данных и выполнение запроса. Так же при этом производится обработка исключительных ситуаций. Исключительная ситуация, это ситуация при которой дальнейшая корректная работа программы невозможна, без специальной обработки этой ситуации. В контексте приведенного примера исключительной ситуацией является невозможность подключения к БД, при этом, как понимаете, дальнейшее использование mysql_query() вряд ли будет корректным.
В примере осуществляется генерация (бросок) и перехват исключения. Генерация осуществляется с помощью инструкции:
throw new Exception('Не коннектиццо'); // Создание объекта класса Exception и выброска его в качестве исключения
Перехват осуществляется с помощью блоков try-catch. Если выполняемый внутри блока try код выбрасывает исключение, дальнейшее выполнение передается блоку catch, который осуществляет обработку.
В случае невозможности подключения к серверу, выбрасывается исключение и управление, минуя попытку выбора базы, передается блоку catch. Обработчик исключения банален: вывод сообщения и завершение сценария. Если блок try отработал нормально (подключились и выбрали базу), управление не попадает в catch, а сразу идет дальше - к выполнению запроса.
Можно не использовать блоки try-catch:
if (!mysql_connect('xxx', 'xxx', 'xxx')) {
throw new Exception('…');
}
Данное исключение не будет обработано и вызовет завершение сценария с выводом сообщения.
Смысл всего этого
Классический пример он на то и классический, что абсолютно ничего не объясняет. Все тоже самое гораздо легче делается старым дедовским методом:
mysql_connect('xxx', 'xxx', 'xxx') or die('Ошибка');
Однако, важнейшим свойством перехвата исключений является то, что внутри блока try могут быть вызовы функций. И все исключения выброшенные внутри них, могут быть перехвачены этим блоком (если не были перехвачены внутри этих самых функций).
Например, займемся любимым делом всех php-программистов - напишем собственный класс для работы с БД. Ну и первым методом у него, конечно, будет подключение к базе.
class DB
{
/**
* Подключение к базе данных
* Параметры подключения жестко заданы в теле метода
*
* @exception Exception невозможно подключение или выбор базы
*/
public static function connect()
{
self::$linkId = mysql_connect('xxx', 'xxx', 'xxx');
if (self::$linkId === false) {
throw new Exception('Нет соединения');
}
if (!mysql_select_db('xxx', self::$linkId)) {
throw new Exception('Нет базы');
}
return true;
}
/**
* Идентификатор подключения
*
* @var Resource
*/
private static $linkId;
}
Класс DB, это библиотека. Мы её напишем, задокумментируем и положим в папочку для библиотек, либо выложим в интернет для бесплатного использования всеми, кто пожелает. А может платного.
Данная библиотека может быть использована в совершенно различных системах, сайтах и т.п. Мы не можем внутри неё вызвать die() - с какой стати библиотека будет убивать весь сценарий? Мы так же не можем осуществить практически никакой другой обработки - вывода сообщения, попытки подключиться под другими параметрами, записи в журнал логов. Потому что мы не знаем структуры системы в которой этот класс будет использован.
Да мы и не обязаны ничего этого делать. Наше дело подключиться к базе. Не получилось - выкидываем исключение. Как его обрабатывать, это дело вызывающего сценария. Например:
try {
DB::connect();
} catch (Exception $e) {
print 'Хм, ошибка вышла';
exit();
}
Отличие от разбора результата
Раньше (да часто и сейчас) подобные вещи реализовывались путем возвращения определенного значения сигнализирующего об ошибке (обычно false) и разбора его в вызвавшем коде.
if (mysql_connect() === false) {
die('Ошибка');
}
mysql_query('…');
Однако, исключения по сравнению с данным методом имеют следующие преимущества:
1. Это более правильно :) . Результат функции, это результат функции, а ошибка, это ошибка. Функция может возвращать любое значение (например, unserialize) и какое из них считать ошибочным непонятно. Так же простой false не несет никакой информации об ошибки, если такая информация может понадобиться, приходится прибегать к передаваемым по ссылке аргументам (см. errno и errstr в fSockOpen()).
2. При использовании метода с false программист обязан каждый раз проверять результаты подобных функций и обрабатывать ошибки. Если программист забыл или поленился (а забывают и ленятся программисты регулярно), то последствия могут быть печальными. При использовании же исключений ситуация прямо противоположная. Исключительные ситуации в случае по умолчанию приводят к завершению сценария с выводом сообщения об ошибке. И только в том конкретном случае, где разработчик озаботился обработкой ошибки, возможна дальнейшая работа.
3. Разматывание стека функций
Разматывание стека функций
Как должно быть известно, функции могут вызывать другие функции, те в свою очередь третьи и т.п. При этом в программе образуется стек функций. В случае возникновения исключения вне блока try, текущая функция завершается и управление передается обратно в вызвавшую. Если вызов функции происходил в блоке try - происходит обработка, если вне - данная функция также завершается и управление передается уже в вызвавшую её. Таким образом стек функций разматывается назад в поисках блока-перехватчика (try). Если домотали до самого верха, не встретив try, происходит обработка по умолчанию - завершение сценария с выводом сообщения об ошибке.
Допустим, пишем мы дальше наш класс DB и задумываемся: а зачем нам явно вызывать метод connect()? И лишнее действие и с базой мы не так активно работаем - бывают сценарии, где вообще база не нужна и подключение будет лишней тратой времени и ресурсов. Подключение нам нужно только при запросе, так давайте его в запрос и вынесем:
/**
* Выполнение запроса
*
* @param string $query запрос
* @return resource ответ
* @exception Exception нет коннекта
* @exception Exception ошибка в запросе
*/
public static function query($query)
{
if (!self::$linkId) {
self::connect(); // Подключаемся, если еще не подключены
}
$res = mysql_query($query);
if (mysql_errno()) {
throw new Exception('Error "'.(htmlSpecialChars($query)).'":'.htmlSpecialChars(mysql_error()));
}
return $res;
}
Здесь еще один throw - генерация исключения при неверном запросе.
А что произойдет при неудачном подключении? Метод connect() выбросит исключение, которое не будет обработано в нем самом (нет try). Connect() завершится и управление вернется в вызвавший его query(). Т.к. здесь так же нету try, управление выйдет и отсюда и передастся коду, вызвавшему query().
Таким образом query() не приходится заморачиваться над обработкой исключений в вызываемых функциях. При использовании false-результата, код query должен был быть таким:
if (!self::$linkId) {
if (!self::connect()) {
return false;
}
}
В то же время код, вызвавший query может быть тоже неосновным - вызываемым другим методом, которому так же не интересны произошедшие ошибки. Да например, это метод selectRow того же класса DB, выбирающий строку по заданному id и вызывающий для этого query(). Добавьте сюда необходимость идентификации типа ошибки и сохранения дополнительной информации о ней и получите столько лишнего геморроя, сколько вам и не снилось. С помощью же исключений все решается элементарно.
Пользовательские типы исключений
В примерах мы использовали в качестве объекта-исключения объект встроенного класса Exception. Однако мы можем наследовать от него свой класс и использовать его. Одно из преимуществ такого подхода - возможность обработки различных типов исключений отдельно друг от друга.
Наследуем (без определения дополнительных методов) два класса исключений - ошибка подключения и ошибка в запросе:
class ExceptionDBConnect extends Exception {}
class ExceptionDBQuery extends Exception {}
Теперь при неудачном подключении будем кидать исключение определенного типа:
self::$linkId = mysql_connect('xxx', 'xxx', 'xxx');
if (self::$linkId === false) {
throw new ExceptionDBConnect('');
}
При ошибочном запросе, соответственно, используем throw new ExceptionDBQuery()
Допустим, причина ошибки при запросе может быть одна - синтаксическая ошибка в SQL. Данную ошибку не следует перехватывать, т.к. она должна быть отображена программисту и исправлена еще на этапе разработки. А вот невозможность подключения к БД - вполне возможная ошибка уже на этапе действующего проекта. Мы должны по этому поводу вывести какое-то сообщение посетителям и, возможно, послать письмо админу.
try {
DB::query('…');
} catch (ExceptionDBConnect $e) {
// Обработка ошибки подключения
}
Таким образом мы перехватываем исключения типа ExceptionDBConnect, все же другие исключения не обрабатываются, они либо вызывают завершение сценария, либо проваливаются ниже, если есть куда.
Можно и по другому:
try {
DB::query('…');
} catch (ExceptionDBConnect $e) {
// Обработка ошибки подключения
} catch (ExceptionDBQuery $e) {
// Обработка ошибки в запросе
} catch (Exception $e) {
// Обработка всех общих исключений,
// а так же всех типов исключений наследуемых от Exception.
// Т.е. абсолютно всех исключений, не обработанных в предыдущих блоках catch.
}
Кроме перехватов различных типов исключений, мы можем дополнить свой класс исключения дополнительной функциональностью. Методы реализованные в стандартном классе Exception можно найти в доке. При генерации исключения при ошибочном запросе, нам приходилось формировать строку из запроса и ошибки, чтобы вывести наиболее полную информацию. Сделаем это более удобно:
class ExceptionDBQuery extends Exception
{
public function __construct($query, $errNo, $errString) {
$this->query = $query;
$this->errNo = $errNo;
$this->errString = $errString;
parent::__construct('', 0);
}
public function getQuery() { return $this->query; }
public function getErrNo() { return $this->errNo; }
public function getErrString() { return $this->errString; }
public function __toString()
{
return
'Ошибка в запросе "'.htmlSpecialChars($this->query).'". '.
'Код ошибки: '.$this->errNo.'. '.
'Описание: "'.htmlSpecialChars($this->errString).'".';
}
private $query;
private $errNo;
private $errString;
}
Генерируем исключение теперь следующим образом:
if (mysql_errno()) {
throw new Exception($query, mysql_errno(), mysql_error());
}
При обработке исключения данного типа, мы можем получить строку запроса и информацию об ошибках с помощью методов типа $e->getQuery(). Метод __toString() формирует стандартное сообщение об ошибке.
Перехват всех исключений в программе
Можно вести журнал всех фатальных исключений в программе. Однако, вместо того, чтобы обрабатывать каждое возможное исключение, писать лог и завершать сценарий, можно просто поставить перехват исключений на всю программу:
/**
* Исполняемый сценарий
*/
try {
/* Изначальный код сценария */
require_once('…');
require_once('…');
$app = new Application();
$app->run();
} catch (Exception $e) {
// Логирование ошибки
}
Или мы хотим в случае неверного запроса к базе выводить сообщение в специальной табличке. Можно обрабатывать каждый запрос, а можно:
try {
// Код программы
} catch (ExceptionDBQuery $e) {
// Вывод информации
}
Использование исключений
Исключения впервые были реализованы в компилируемых языках. Там они не получили слишком широкого применения, т.к. требуют генерации большого количества дополнительного кода и снижают скорость выполнения. В интерпретируемых языках, типа PHP, на скорость выполнения исключения практически не влияют. Однако, в PHP они используются все-таки не так сильно, как этого заслуживают.
Гораздо активнее исключения использует, например, Python, практически на каждом шагу. Например, функция открытия файла в нем не возвращает никаких false при ошибке, а генерирует исключение. И правильно - в случае отсутствия файла сценарий обычно завершается и только, если программист решил явно обработать подобную ситуацию возможно продолжение программы.