|
Если особая ситуация ожидалась, была перехвачена и не оказала плохого воздействия на ход программы, то стоит ли ее называть ошибкой? Так говорят только потому, что программист думает о ней как об ошибке, а механизм особых ситуаций является средством обработки ошибок. С другой стороны, особые ситуации можно рассматривать просто как еще одну структуру управления. Подтвердим это примером:
class message { /* ... */ }; // сообщение class queue { // очередь // ... message* get(); // вернуть 0, если очередь пуста // ... }; void f1(queue& q) { message* m = q.get(); if (m == 0) { // очередь пуста // ... } // используем m }
Этот пример можно записать так:
class Empty { } // тип особой ситуации "Пустая_очередь" class queue { // ... message* get(); // запустить Empty, если очередь пуста // ... }; void f2(queue& q) { try { message* m = q.get(); // используем m } catch (Empty) { // очередь пуста // ... } }
В варианте с особой ситуацией есть даже какая-то прелесть. Это хороший пример того, когда трудно сказать, можно ли считать такую ситуацию ошибкой. Если очередь не должна быть пустой (т.е. она бывает пустой очень редко, скажем один раз из тысячи), и действия в случае пустой очереди можно рассматривать как восстановление, то в функции f2() взгляд на особую ситуацию будет такой, которого мы до сих пор и придерживались (т.е. обработка особых ситуаций есть обработка ошибок). Если очередь часто бывает пустой, а принимаемые в этом случае действия образуют одну из ветвей нормального хода программы, то придется отказаться от такого взгляда на особую ситуацию, а функцию f2() надо переписать:
class queue { // ... message* get(); // запустить Empty, если очередь пуста int empty(); // ... }; void f3(queue& q) { if (q.empty()) { // очередь пуста // ... } else { message* m = q.get(); // используем m } }
Отметим, что вынести из функции get() проверку очереди на пустоту
можно только при условии, что к очереди нет параллельных обращений.
Не так то просто отказаться от взгляда, что обработка особой
ситуации есть обработка ошибки. Пока мы придерживаемся такой точки
зрения, программа четко подразделяется на две части: обычная часть
и часть обработки ошибок. Такая программа более понятна. К сожалению,
в реальных задачах провести четкое разделение невозможно, поэтому
структура программы должна (и будет) отражать этот факт. Допустим,
очередь бывает пустой только один раз (так может быть, если функция
get() используется в цикле, и пустота очереди говорит о конце цикла).
Тогда пустота очереди не является чем-то странным или ошибочным.
Поэтому, используя для обозначения конца очереди особую ситуацию,
мы расширяем представление об особых ситуациях как ошибках. С другой
стороны, действия, принимаемые в случае пустой очереди, явно отличаются
от действий, принимаемых в ходе цикла (т.е. в обычном случае).
Механизм особых ситуаций является менее структурированным,
чем такие локальные структуры управления как операторы if или for.
Обычно он к тому же является не столь эффективным, если особая
ситуация действительно возникла. Поэтому особые ситуации следует
использовать только в том случае, когда нет хорошего решения с более
традиционными управляющими структурами, или оно, вообще, невозможно.
Например, в случае пустой очереди можно прекрасно использовать для
сигнализации об этом значение, а именно нулевое значение указателя на
строку message, значит особая ситуация здесь не нужна. Однако, если бы
из класса queue мы получали вместо указателя значение типа int, то
то могло не найтись такого значения, обозначающего пустую очередь.
В таком случае функция get() становится эквивалентной операции
индексации из $$9.1, и более привлекательно представлять пустую очередь
с помощью особой ситуации. Последнее соображение подсказывает, что
в самом общем шаблоне типа для очереди придется для обозначения пустой
очереди использовать особую ситуацию, а работающая с очередью функция
будет такой:
void f(Queue<X>& q) { try { for (;;) { // ``бесконечный цикл'' // прерываемый особой ситуацией X m = q.get(); // ... } } catch (Queue<X>::Empty) { return; } }
Если приведенный цикл выполняется тысячи раз, то он, по всей
видимости, будет более эффективным, чем обычный цикл с проверкой
условия пустоты очереди. Если же он выполняется только несколько
раз, то обычный цикл почти наверняка эффективней.
В очереди общего вида особая ситуация используется как способ
возврата из функции get(). Использование особых ситуаций как способа
возврата может быть элегантным способом завершения функций поиска.
Особенно это подходит для рекурсивных функций поиска в дереве. Однако,
применяя особые ситуации для таких целей, легко перейти грань разумного
и получить маловразумительную программу. Все-таки всюду, где это
действительно оправдано, надо придерживаться той точки зрения, что
обработка особой ситуации есть обработка ошибки. Обработка ошибок по
самой своей природе занятие сложное, поэтому ценность имеют любые
методы, которые дают ясное представление ошибок в языке и способ
их обработки.