Джоэл Спольски снова наехал на exception'ы (они же — исключения) :-).

Кто не в курсе, некоторое время назад он уже писал, почему, по его мнению, exception'ы — это плохо. К его мнению присоединился знаменитый программист из Microsoft Реймонд Чен. Однако, похоже, что весь остальной интернет против :-). Если интересно, можно много чего найти на эту тему по словам "Joel Spolsky exceptions", но мне, например, очень понравилась статья Неда Батчелдера.

Я думаю, что одних только двух мнений Джоэла и Реймонда вполне достаточно для того, чтобы склонить на свою сторону массу программистов, и не только начинающих. Поэтому я решил набраться наглости и написать статью о том, что я сам думаю про исключения, чтобы попытаться отвратить неокрепшие умы от такого необдуманного поступка :-).

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

Красота исключений

Предположим, у нас есть две функции, которые создают, а потом обрабатывают какие-то данные, лежащие в структуре. Традиционный код:

...
int result=0;
SomeData data; //какая-нибудь структура
result=createSomeData(&SomeData);
if(result<0) {
  cleanup();
  return result;
}
result=processSomeData(SomeData);
if(result<0) {
  cleanup();
  return result;
}
cleanup();
return 0;

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

Код с исключениями:

try {
  ...
  processSomeData(createSomeData());
} finally {
  cleanup();
}

Тут я тихо перешел на Java, потому что в C++, насколько я знаю, нет finally. Но на смысл это не влияет, finally можно сымитировать в catch выбрасыванием опять того же исключения.

Написание такого простого код становится возможным по двум причинам.

Что же не так

У обоих гуру основной мыслью в статьях по всему этому поводу проходит то, что вызов исключений скрыт от глаз человека, читающего программу. Джоэл даже обозвал исключения "скрытым goto", и сказал, что они хуже goto, потому что тут не видно ни того, откуда оно вызывается, ни куда выполнение кода в итоге попадет. В качестве примера приводится такой вод псевдокод:

dosomething();
cleanup();

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

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

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

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

В большинстве случаев код, вызывающий функции, понятия не имеет о том, что делать с большинством возвращаемых оттуда ошибок.

Хороший пример — это реакция на нехватку памяти. У самого Джоэла притчей во языцех стало то, что нерадивые программисты забывают проверять значение, возвращенное из malloc(). Ну хорошо, проверили мы, что malloc вернул "памяти нет". Что делать? Только отказываться от дальнейшей работы этой функции, что тут другое придумать? Вот исключения и делают это автоматически и надежно, не полагаясь на то, что каждая из цепочки в 50 вызывающих функций не забудет передать наверх каждую ошибку, возникшую в самой глубине менеджера памяти.

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

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

Проблемы с исключениями

Проблемы появляются тогда, когда начинается смешение подходов: старого — процедурного с кодами возвратов — и нового — объектного с исключениями. Мне кажется, я понимаю, откуда растут ноги у мнения Джоэла и Реймонда. Видимо, это от того, что они насмотрелись на кучу кода, в котором исключения использовались просто неправильно. И это немудрено, потому что это целая новая парадигма, и отвыкнуть от if(result!=0) не проще, чем, скажем, от передачи десятка структур в параметры функции, вместо того, чтобы использовать нормально спроектированные объекты.

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

try {
  dosomething();
} catch(...) {
  //do nothing
}

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

Еще одна проблема проявляется в том, что в какой-то отдельной функции появляется несколько вложенных конструкций try..catch, try..finally, и где-то там в перекрестье нагромождения фигурных скобочек или begin'ов с end'ами затерялась пара строчек реального кода. То есть, это совсем не то, чего мы хотели добиться исключениями: чтобы код было попроще читать.

Это верный признак того, что программа в этом месте не очень хорошо спроектирована. Если одной функции приходится занимать слишком много разных ресурсов и освобождать их в finally, а также обрабатывать слишком много разных по смыслу ошибок, то это означает, что функция сама по себе делает слишком много. Воспользуйтесь рефакторингом и разбейте функцию на более простые. Вообще говоря, при правильном проектировании объектов количество всеразличных подчисток ресурсов вообще сводится к минимуму. Чаще всего хороший "нежадный" класс резервирует все необходимые ему ресурсы в конструкторе, а освобождает в деструкторе. Поэтому при использовании его функций вообще не надо заботится ни о занятии, ни об освобождении.

В заключение

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

Комментарии: 8

  1. Pavel Titov

    У него еще есть впечатляющие масштабностью рассуждения об обработке введенных пользователем данных. Я понимаю, что в VBScript, на котором он пишет нет tainting'а, но он то пишет так, как будто его нет нигде...

  2. [...] енной IOResult с кодом возврата. А исключения — это более надежная парадигма программирования. Кром�[...]

  3. Сергей

    С исключениями разобрался в пух и прах :)
    На самом деле проблема в том, что любой подход нужно не просто использовать, а использовать правильно.
    А что скажешь на вот такую штуку - паблик конструкторы?
    http://dotnet.osypchuk.com/2005/06/public-constructors-are-evil-articles.html ?
    Сорри, чё-то я решил писать на английском, может и не прав.

  4. Макс

    Угумс. Я когда-то писал о том же.

  5. Alex Efros

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

    Всё остальное (как правильно и как не правильно пользоваться исключениями) - это детали и нюансы. Главное - вопрос пользоваться или нет уже не стоит. Если исключения используются не правильно, значит надо учится пользоваться ими правильно, а не отказываться от них вообще.

  6. someone

    Я не противник исключений. Но слишком часто встретилось "не думать".

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

    SomeData data;
    int result = createSomeData(SomeData);
    if (result == 0)
    {
    result = processSomeData(SomeData);
    }
    cleanup();
    return result;

    То, что этот код возвращает результат своей работы, считаю важным.

    "В большинстве случаев код, вызывающий функции, понятия не имеет о том, что делать с большинством возвращаемых оттуда ошибок."
    Задуматься в этом месте нужно не о том, что передача ошибки вверх это рутина. А о том, что есть ошибки, которые вызывающий код знает, как обработать.

    Еще хотелось бы обратить внимание автора на то, что он противопоставил: “старого — процедурного с кодами возвратов — и нового — объектного с исключениями”. Почему именно коды возвратов? Используйте объектный, но без исключений.

  7. ARSolog

    Да. Помню, когда только начал знакомиться с с# читал о том как прикольно с исключениями и что они могут. Однако как правильно применять - нада читать маститых профи как sm.org

    Спасибо за Ваш блог. Начитаться не могу. Столько полезнейший инфы.

    И одна поправочка - даже в современных языках программирования заточенных под исключения огромное кол-во фун-ций работающих по-старинке и возвращающих коды. Поэтому и приходиться "смешивать", ибо, с одной стороны исключения это обьекты и на их "генерирование" (создание) тратятся ресурсы а это быстродействие (поэтому ставить блок ифов или кейсов для того что-бы сгенерировать исключение улетящее наверх к такому-же монструозному блоку катч...), с другой стороны быстрее разобраться с ошибками "на месте" а не выбирать в какой вызывающей функции ставить тру/катч/финал (так что если malloc говорит "памяти нет" - вываливается в ОС с сообщением нот_енофф_мемори).

  8. Евгений Морозов

    Я работал в проекте, где намеренно не использовались исключения. Проект большой, около 50000 строк на Perl. В таком проекте использование проверок кодов возврата вместо исключений превращается в самый настоящий кошмар и источник ошибок.

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

    В итоге там эмулировалось нечто вроде исключений, только на кодах ошибок и глобальных переменных. Ужас, как вспомню, дрожь пробирает. Я не выдержал и уволился, потому что слушать меня никто не хотел. Все мои попытки хоть как-то улучшить данного монстра натыкались на непонимание и саботаж.

Добавить комментарий