Перехват исключений по базовому классу
От: FrozenHeart  
Дата: 20.08.15 09:04
Оценка:
Приветствую.

Почему все настолько плохо относятся к практике перехвата исключений по базовому классу?

Вот, что я имею ввиду:

try
{
  DoSmth();
}
catch (Exception ex)
{
  Console.WriteLine(ex.Message);
}


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

Рассмотрим пример.

Мне необходимо вызвать метод ZipFile.CreateFromDirectory. Смотрим, что он может выбросить:

ArgumentException
ArgumentNullException
PathTooLongException
DirectoryNotFoundException
IOException
UnauthorizedAccessException
NotSupportedException


Допустим, валидность аргументов я буду проверять перед вызовом данного метода самостоятельно, так что первые три типа исключений (ArgumentException, ArgumentNullException и PathTooLongException) сразу же отпадают. Существование директории лучше не проверять предварительно, ведь в общем случае добиться атомарности в данном случае всё равно не получится:

if (Directory.Exists(path))
{
  // Директория удаляется
  ZipFile.CreateFromDirectory(path, "some.zip"); // Выбрасывается исключение из-за отсутствия sourceDirectory
}


Остальные типы исключений (IOException, UnauthorizedAccessException и NotSupportedException) также могут возникнуть из-за каких-то внешних обстоятельств, проверять которые заранее мы либо запаримся, либо просто не сможем атомарно, как в предыдущем случае.

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

Подливают масла в огонь ещё и third-party libraries, по документации которых (если она вообще есть) далеко не всегда понятно, какие именно типы исключений какие методы выбрасывают. Лезть в реализацию? Во-первых, она на всегда доступна, а, во-вторых, надеяться на такую информацию, я считаю, не стоит. Проще уж ловить Exception.

Ещё один момент. Что если разработчики используемой Вами библиотеки / фреймворка решат добавить с новой версией дополнительные типы исключений? Например, было исключение NotSupportedException, которое возникало по двум следующим причинам:

NotSupportedException

sourceDirectoryName or destinationArchiveFileName contains an invalid format.

-or-

The zip archive does not support writing.


А теперь представьте, что его "разбили" на два типа -- InvalidFormatException и ZipArchiveDoesNotSupportWritingException. И это хорошо, если они оба будут наследоваться от NotSupportedException! А что, если нет? Читать changelon? А что, если упустите эту информацию из виду? Писать юнит-тесты? Это тоже не идеальный инструмент, т.к. их составляют люди, и они вполне могут забыть проверить какую-то определённую ситуацию.

Единственная проблема, которая мне приходит на ум -- это возможный перехват "лишних" исключений (например, перехват нажатия Ctrl-C или отмены таска), но для таких случаев многие библиотеки и фреймворки специально создают отдельную иерархию классов, которая начинается не с Exception.

В общем, что Вы думаете по этому поводу? Как у Вас в компании относятся к перехвату исключений по базовому классу и какие правила для этого заведены?
Re: Перехват исключений по базовому классу
От: DarkEld3r  
Дата: 20.08.15 09:23
Оценка: 5 (1) +1
Здравствуйте, FrozenHeart, Вы писали:

FH>Почему все настолько плохо относятся к практике перехвата исключений по базовому классу?

По моему, всё зависит от того, что с исключением делать собираешься. Если более конкретные исключения можно/нужно как-то особо обрабатывать, то естественно надо и отдельно ловить. Если же логика будет одинаковой, то обрабатывать именно базовый класс вполне естественно.

Другое дело, что ловить исключение просто чтобы проигнорировать, как правило, плохо.
Re: Перехват исключений по базовому классу
От: Sinix  
Дата: 20.08.15 09:35
Оценка: 1 (1) +1
Здравствуйте, FrozenHeart, Вы писали:

FH>Вот, что я имею ввиду:

Перехватывать стоит только те исключения, которые знаешь как обрабатывать. Т.е. в примере с Zip-архивом: IOException (заодно и все потомки отловятся) + UnauthorizedAccessException. Остальное говорит скорее о багах в коде, чем об обрабатываемой ситуации.

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


Определиться с тем, что отлавливать, очень просто: представляем, что помимо DoSmth() есть метод TryDoSmth(). Прикидываем, в каких случаях TryDoSmth() должен возвращать false, в каких — падать, ловим исключения из первой группы.
А ещё лучше — не представлять, а написать этот метод самому. По крайней мере, не придётся исправлять по всему коду, если промахнёшься с выбором.


P.S. Классика на ту же тему.

P.P.S. Тут нужно ещё разделять чисто инфраструктурный код, в котором стандарт — "Где-то ошибка? Падай, код выше по стеку разберётся". И бизнес-логику, в которой ожидаемое поведение слегка другое: при ошибке откатываем все изменения + выдаём пользователю диагностику. Для каждого из вариантов подходы будут разные.
Re: Перехват исключений по базовому классу
От: Кодт Россия  
Дата: 20.08.15 10:26
Оценка: 5 (1) +1
Здравствуйте, FrozenHeart, Вы писали:

FH>Почему все настолько плохо относятся к практике перехвата исключений по базовому классу?


Все — это кто?

FH>Мне необходимо вызвать метод ZipFile.CreateFromDirectory. Смотрим, что он может выбросить:

<>
FH>Теперь представьте (и в моём случае это вполне частая задача), что реагировать на все типы исключений мне необходимо одним и тем же образом (например, писать текст исключения в лог и завершать выполнение текущего метода). Чем в таком случае плох принцип перехвата исключений по базовому классу (в данном случае Exception)?

Если проблема лишь в кривой, слишком расплющенной, иерархии исключений, — то это вопрос к автору библиотеки.
У С++ стандартные исключения образуют иерархию.

Вообще же, механизм исключений в некоторых языках сделан "шоб было", и для комфортной работы с ними придумывают всякие хаки.
Например, кейс
void CommonCatches() {
  try { throw; } // ожидается, что CommonCatches вызывается из catch-блока. этот хак ретранслирует исключение в местные catch-блоки
  catch(const E1& e) { .......... }
  catch(const E2& e) { ..........; throw; } // если надо, можем и выбросить обратно
  catch(const E3& e) { .......... }
  catch(const exception& e) { ..........; throw; }
  catch(...) { MoreCommonCatches(); } // пакеты обработчиков могут образовывать цепочку
}

void YourFunction() {
  try {
    try { .......... }
    catch(...) { CommonCatches(); } // обрабатываем известным способом
  }
  catch(const E2& e) { .......... } // в том числе, недообработанное ранее исключение
  catch(const E4& e) { .......... }
  catch(const E5& e) { .......... }
}


FH>Подливают масла в огонь ещё и third-party libraries, по документации которых (если она вообще есть) далеко не всегда понятно, какие именно типы исключений какие методы выбрасывают. Лезть в реализацию? Во-первых, она на всегда доступна, а, во-вторых, надеяться на такую информацию, я считаю, не стоит. Проще уж ловить Exception.


Если ты не знаешь, какие исключения кидает библиотека, значит, тебе это не нужно.
Максимум, что можешь сделать — это написать finally-код (RAII, using, что там ещё есть в любимом языке).

FH>Ещё один момент. Что если разработчики используемой Вами библиотеки / фреймворка решат добавить с новой версией дополнительные типы исключений? Например, было исключение NotSupportedException, которое возникало по двум следующим причинам:

FH>А теперь представьте, что его "разбили" на два типа -- InvalidFormatException и ZipArchiveDoesNotSupportWritingException. И это хорошо, если они оба будут наследоваться от NotSupportedException! А что, если нет? Читать changelon? А что, если упустите эту информацию из виду? Писать юнит-тесты? Это тоже не идеальный инструмент, т.к. их составляют люди, и они вполне могут забыть проверить какую-то определённую ситуацию.

А если разработчики рукожопые пионеры, которые в ударе/угаре сломали обратную совместимость, то почему ты думаешь, что они только в этом месте нарушили старые контракты?


FH>В общем, что Вы думаете по этому поводу? Как у Вас в компании относятся к перехвату исключений по базовому классу и какие правила для этого заведены?


http://google-styleguide.googlecode.com/svn/trunk/cppguide.html#Exceptions

We do not use C++ exceptions.

No woman no cry.
Перекуём баги на фичи!
Re: Перехват исключений по базовому классу
От: vsb Казахстан  
Дата: 20.08.15 10:41
Оценка: +1
В приведённом примере вы не можете гарантировать, что вы проверите аргументы методов. Может проверите, а может опечатаетесь и пропустите баг. Если вы будете ловить все исключения, то вы про этот баг можете и не узнать. А если будете ловить только те, которые ожидаете, то про этот баг вы узнаете и исправите его.

Если ваша библиотека так ломает свои внешние интерфейсы, это не очень хорошая библиотека и от неё лучше избавиться.

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

В общем и целом ловля всех исключений это порочная практика. Она существует давно и у неё много своих сторонников. Не проверять коды возврата, подавлять исключения, и тд. Мол софт работает без падений, пользователь не видит ошибок и тд. Я категорически против такой практики.
Re[2]: Перехват исключений по базовому классу
От: LaPerouse  
Дата: 20.08.15 10:45
Оценка: +1
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, FrozenHeart, Вы писали:


S>P.P.S. Тут нужно ещё разделять чисто инфраструктурный код, в котором стандарт — "Где-то ошибка? Падай, код выше по стеку разберётся". И бизнес-логику, в которой ожидаемое поведение слегка другое: при ошибке откатываем все изменения + выдаём пользователю диагностику. Для каждого из вариантов подходы будут разные.


В бизнес-логике все то же самое, никаких отличий. Обрабатывать надо только там, где это необходимо методу для выполнения своего контракта. Если структура данных осталась в неконистистентном состоянии, это проблема вызывающей стороны. То есть design by contract в самом очевидном виде. Но стоит сказать, что в самой распространенной STM-модели бизнес-логики (то есть изменения копии данных с атомарной фиксацией) откат транзакции по Exception — это единственно разумный паттерн (иное показатель — криворукости) и помимо того реализуется всеми автоматическими менеджерами транзакций. В shared memory бизнес логика должна быть не потокобезопасна, вся синхронизация должна производится снаружи. Да, шальной exception может привести к потере целостности in-memory модели, но практика показывает, что лучше в этом случае убить полностью логическую единицу (то есть игру, коммуникацию и т.п.), так как это свидтельствует о явной ошибке, нежели пытаться восстанавливаться.
Социализм — это власть трудящихся и централизованная плановая экономика.
Re[2]: Перехват исключений по базовому классу
От: FrozenHeart  
Дата: 20.08.15 10:57
Оценка:
DE>По моему, всё зависит от того, что с исключением делать собираешься. Если более конкретные исключения можно/нужно как-то особо обрабатывать, то естественно надо и отдельно ловить. Если же логика будет одинаковой, то обрабатывать именно базовый класс вполне естественно.

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

DE>Другое дело, что ловить исключение просто чтобы проигнорировать, как правило, плохо.


Почему же? Зависит от задачи.
Например, не смогли отправить пакет при помощи TCP -- да и ладно, следующий отправим (если получателю необходимо знать лишь конечное состояние).
Re[2]: Перехват исключений по базовому классу
От: FrozenHeart  
Дата: 20.08.15 10:59
Оценка:
S>Перехватывать стоит только те исключения, которые знаешь как обрабатывать. Т.е. в примере с Zip-архивом: IOException (заодно и все потомки отловятся) + UnauthorizedAccessException. Остальное говорит скорее о багах в коде, чем об обрабатываемой ситуации.

А что по поводу NotSupportedException, например? Это разве говорит о багах в коде?

NotSupportedException

sourceDirectoryName or destinationArchiveFileName contains an invalid format.

-or-

The zip archive does not support writing.

Re[3]: Перехват исключений по базовому классу
От: Sinix  
Дата: 20.08.15 11:01
Оценка: +1
Здравствуйте, LaPerouse, Вы писали:

LP>В бизнес-логике все то же самое, никаких отличий.


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

Как правило такие вещи решают без отлова исключений, откатом неудачных действий в finally, но всякое бывает
Re[2]: Перехват исключений по базовому классу
От: FrozenHeart  
Дата: 20.08.15 11:04
Оценка:
К>Все — это кто?

Аудитория различных форумов, которые уже при виде

try:
  do_smth()
except Exception:
  # ...


начинают плеваться в монитор.

К>Если проблема лишь в кривой, слишком расплющенной, иерархии исключений, — то это вопрос к автору библиотеки.


Ну, я привёл пример с ZipFile.CreateFromDirectory из .NET Framework'а. Там кривая иерархия исключений?

К>У С++ стандартные исключения образуют иерархию.


Но проблема одна и та же.

К>Если ты не знаешь, какие исключения кидает библиотека, значит, тебе это не нужно.


Что это значит? Не ловить исключения вообще?

К>А если разработчики рукожопые пионеры, которые в ударе/угаре сломали обратную совместимость, то почему ты думаешь, что они только в этом месте нарушили старые контракты?


Тут согласен, да.

К>http://google-styleguide.googlecode.com/svn/trunk/cppguide.html#Exceptions

К>

К>We do not use C++ exceptions.

К>No woman no cry.

Свои минусы этот подход тоже имеет. Прокидывать return code'ы на несколько уровней вверх? Жесть.
Re: Перехват исключений по базовому классу
От: Mr.Delphist  
Дата: 20.08.15 11:09
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

FH>Почему все настолько плохо относятся к практике перехвата исключений по базовому классу?


Собственно, зависит от того, кто эти все, и что предполагается делать при перехвате.

1) Тупой catch(Exception) { ... do nothing... } готовит для автора личный котёл в аду — мы не понимаем, что произошло, улики уничтожены
2) Охватывающий catch, который логирует произошедшее и терминирует приложение или пробрасывает наружу этот же (или обёртывающий) эксепшн, или код ошибки, выглядит куда более логично

Базовый класс тем и удобен, что можно опираться на некоторый уровень достоверности "моё/чужое", чтобы не схватить чужую ошибку (а то будут думать на нас). Не случайно рекомендуется бросать свои кастомные исключения, наследованные от ApplicationException, но чтобы при этом контекст произошедшего был понятен и в базовом классе (все эти What, Message, ToString, которые должны переопределяться наследниками).

В общем, Барбара Лискоу фигни не посоветует
Re[3]: Перехват исключений по базовому классу
От: Sinix  
Дата: 20.08.15 11:13
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

FH>А что по поводу NotSupportedException, например? Это разве говорит о багах в коде?


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

Если встречается редко — пускай падает с человекочитаемым сообщением об ошибке (т.е. отлавливать на общих правах выше по стеку).
Если часто — придётся обрабатывать как частный случай (т.е. как минимум проверяем на запись или пробуем другое имя).
Re[3]: Перехват исключений по базовому классу
От: LaPerouse  
Дата: 20.08.15 11:14
Оценка: +1
Здравствуйте, FrozenHeart, Вы писали:

S>>Перехватывать стоит только те исключения, которые знаешь как обрабатывать. Т.е. в примере с Zip-архивом: IOException (заодно и все потомки отловятся) + UnauthorizedAccessException. Остальное говорит скорее о багах в коде, чем об обрабатываемой ситуации.


FH>А что по поводу NotSupportedException, например? Это разве говорит о багах в коде?


Нет, не говорит. Метод должен обработать эту ситуацию в соответствии со своим контрактом. Например, ничего не сделать или выдать исключение (желательно на своем логическом уровне, а не какой нибудь IOException нижележащей библиотеки).
Социализм — это власть трудящихся и централизованная плановая экономика.
Re[3]: Перехват исключений по базовому классу
От: Кодт Россия  
Дата: 20.08.15 11:24
Оценка: +1
Здравствуйте, FrozenHeart, Вы писали:

FH>Аудитория различных форумов, которые уже при виде <> начинают плеваться в монитор.


Ну не знаю, это не "все". Либо (ты) им показывал говнокод, и народ плевался на говнокод как таковой, а не на исключения как таковые.


К>>Если проблема лишь в кривой, слишком расплющенной, иерархии исключений, — то это вопрос к автору библиотеки.

FH>Ну, я привёл пример с ZipFile.CreateFromDirectory из .NET Framework'а. Там кривая иерархия исключений?

Там, кстати, прямая.

ArgumentException
— ArgumentNullException
IOException
— PathTooLongException
— DirectoryNotFoundException
UnauthorizedAccessException
NotSupportedException

Если не различать тонкости, то, как минимум, от трёх обработчиков можно избавиться.


К>>Если ты не знаешь, какие исключения кидает библиотека, значит, тебе это не нужно.

FH>Что это значит? Не ловить исключения вообще?

Да вообще не программировать, что уж.

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


К>>No woman no cry.

FH>Свои минусы этот подход тоже имеет. Прокидывать return code'ы на несколько уровней вверх? Жесть.

Во-первых, это повод уменьшать количество уровней, ответственных за обработку проблемы.
Во-вторых, вода дырочку найдёт. Есть всякие обходные способы и (анти)паттерны — например, заменить исключения сигналами/слотами разной степени долгоживучести.
Перекуём баги на фичи!
Re: Перехват исключений по базовому классу
От: icWasya  
Дата: 20.08.15 12:16
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

FH>Приветствую.


FH>Почему все настолько плохо относятся к практике перехвата исключений по базовому классу?


FH>Вот, что я имею ввиду:


FH>
FH>try
FH>{
FH>  DoSmth();
FH>}
FH>catch (Exception ex)
FH>{
FH>  Console.WriteLine(ex.Message);
FH>}
FH>

... и какие правила для этого заведены?

Где-то попалась статейка о практике перехвата исключений.
как-то так
Для начала надо разделить исключения по такому признаку — 1)косяки разработчика и 2)косяки пользователя.
По первым надо выудить информацию типа : какое исключение, в каком месте программы, в какой функции, с какими параметрами;
записать в лог и может быть аварийно завершиться. Ловушки на такие исключения ставятся на самом верхнем уровне и их можно ловить по базовому классу.
По вторым нужно как можно ближе к месту возникновения проанализировать, что же пользователь сделал не так,
превратить в понятное пользователю сообщение, выдать это сообщение пользователю и вернуть систему в согласованное состояние.
Вот тут надо ставить ловушки на конкретный тип.
Re[2]: Перехват исключений по базовому классу
От: jazzer Россия Skype: enerjazzer
Дата: 20.08.15 19:21
Оценка:
Здравствуйте, Кодт, Вы писали:

К>http://google-styleguide.googlecode.com/svn/trunk/cppguide.html#Exceptions

К>

К>We do not use C++ exceptions.


Да сколько ж можно это бездумно цитировать...

К>No woman no cry.


Вообще-то у этой фразы другой смысл, нежели "нет человека — нет проблемы"
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
Re: Перехват исключений по базовому классу
От: jazzer Россия Skype: enerjazzer
Дата: 20.08.15 19:22
Оценка:
Здравствуйте, FrozenHeart, Вы писали:

FH>Приветствую.


FH>Почему все настолько плохо относятся к практике перехвата исключений по базовому классу?


Кто это "все"? Я вот очень даже хорошо отношусь.
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
Re[3]: Перехват исключений по базовому классу
От: Кодт Россия  
Дата: 21.08.15 09:30
Оценка:
Здравствуйте, jazzer, Вы писали:

J>Да сколько ж можно это бездумно цитировать...


А я не бездумно цитирую.
Был вопрос "чего придерживаются в вашей компании".

К>>No woman no cry.


J>Вообще-то у этой фразы другой смысл, нежели "нет человека — нет проблемы"


Боб Марли выслал Тане мячик
обратно в Краснодарский край
с пометкой "Выловил. No woman
no cry"

Перекуём баги на фичи!
Re[4]: Перехват исключений по базовому классу
От: jazzer Россия Skype: enerjazzer
Дата: 21.08.15 12:45
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Здравствуйте, jazzer, Вы писали:


J>>Да сколько ж можно это бездумно цитировать...


К>А я не бездумно цитирую.

К>Был вопрос "чего придерживаются в вашей компании".

У вас их не используют по той же причине, что и в опенсорсе для гугла?
Т.е. у вас слишком много древнего овнокода, который писался без единой мысли об исключениях?
Потому что именно это написано в их объяснении запрета на исключения.
А то обычно эту часть как-то опускают, просто пишут, что, мол, Сам Гугл исключения запретил, уж им-то виднее.
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
Re[5]: Перехват исключений по базовому классу
От: Кодт Россия  
Дата: 21.08.15 15:26
Оценка:
Здравствуйте, jazzer, Вы писали:

J>У вас их не используют по той же причине, что и в опенсорсе для гугла?


Для того, чтобы как можно менее болезненно синхронизироваться с гугловским опенсорсом.
Перекуём баги на фичи!
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.