Лучшие практики управления исключениями на Java или C # [закрыто]


117

Я застрял, решая, как обрабатывать исключения в моем приложении.

Во многом мои проблемы с исключениями возникают из-за 1) доступа к данным через удаленную службу или 2) десериализации объекта JSON. К сожалению, я не могу гарантировать успех ни в одной из этих задач (отключение сетевого подключения, искаженный объект JSON, который находится вне моего контроля).

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

Вот пример кода (на JAVA) типичного метода)

public boolean doSomething(Object p_somthingToDoOn)
{
    boolean result = false;

    try{
        // if dirty object then clean
        doactualStuffOnObject(p_jsonObject);

        //assume success (no exception thrown)
        result = true;
    }
    catch(Exception Ex)
    {
        //don't care about exceptions
        Ex.printStackTrace();
    }
    return result;
}

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

Вкратце по ключевым вопросам:

  1. Можно ли просто перехватывать исключения, но не всплывать или формально уведомлять систему (через журнал или уведомление для пользователя)?
  2. Какие существуют передовые практики для исключений, которые не приводят ко всему, что требует блока try / catch?

Продолжить / Редактировать

Спасибо за все отзывы, нашел несколько отличных источников по управлению исключениями в Интернете:

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

Кроме того, следите за гниением кода из-за чрезмерного количества попыток / уловок или отказа от исключения (исключение - предупреждение системы, о чем еще нужно предупредить?).

Кроме того, это прекрасный комментарий от m3rLinEz .

Я склонен согласиться с Андерсом Хейлсбергом и вами в том, что большинство звонящих заботятся только о том, прошла ли операция успешно или нет.

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

  • Какой смысл генерировать это исключение?
  • Как имеет смысл с этим справляться?
  • Действительно ли вызывающий абонент заботится об исключении или его просто волнует, был ли вызов успешным?
  • Является ли изящным принуждение вызывающего абонента к управлению потенциальным исключением?
  • Вы уважаете языковые идомы?
    • Вам действительно нужно возвращать флаг успеха, например логическое? Возврат логического значения (или int) - это скорее мышление C, чем Java (в Java вы просто обрабатываете исключение).
    • Следуйте конструкциям управления ошибками, связанным с языком :)!

Хотя статья сообщества оракулов написана на java, это общий совет для очень широкого класса языков. Хорошая статья, именно то, что я искал.
Bunny Rabbit,

Ответы:


61

Мне кажется странным, что вы хотите перехватывать исключения и превращать их в коды ошибок. Как вы думаете, почему вызывающий абонент предпочел бы коды ошибок исключениям, если последнее является значением по умолчанию как в Java, так и в C #?

Что касается ваших вопросов:

  1. Вы должны перехватывать только те исключения, с которыми действительно можете справиться. В большинстве случаев просто перехватить исключения - не лучший вариант. Есть несколько исключений (например, журналирование и маршаллинг исключений между потоками), но даже в этих случаях вам обычно следует повторно генерировать исключения.
  2. У вас определенно не должно быть много операторов try / catch в вашем коде. Опять же, идея состоит в том, чтобы перехватывать только те исключения, которые вы можете обработать. Вы можете включить обработчик исключений верхнего уровня, чтобы превратить любые необработанные исключения во что-то полезное для конечного пользователя, но в противном случае вам не следует пытаться перехватывать каждое исключение во всех возможных местах.

Что касается того, почему кому-то нужны коды ошибок вместо исключений ... Мне всегда казалось странным, что HTTP по-прежнему использует коды ошибок, хотя мое приложение генерирует исключение. Почему HTTP не может позволить мне передать исключение как есть?
Trejkaz

лучшее, что вы можете сделать, это
заключить

@Trejkaz Вы не хотите возвращать подробности исключения пользователям, потому что это угроза безопасности. Вот почему серверы HTML возвращают коды ошибок. Возврат сообщения об ошибке также связан с проблемой локализации, что, вероятно, приводит к увеличению размера HTML и замедлению возврата. Вот почему я думаю, что HTML-серверы возвращают коды ошибок.
Дидье А.

Я думаю, лучше сказать: «Вы должны подавлять только те исключения, с которыми вы действительно можете справиться».
Дидье А.

@didibus Как вы думаете, почему соображения безопасности должны создавать неудобства в разработке? Я никогда ничего не говорил о продюсировании. Что касается локализации сообщений об ошибках, то на всем веб-сайте уже есть эта проблема, и люди, похоже, с ней справляются.
Trejkaz

25

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

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

В целом я использую следующие правила:

  1. В моих компонентах и ​​библиотеках я перехватываю исключение только в том случае, если я собираюсь обработать его или сделать что-то на его основе. Или, если я хочу предоставить дополнительную контекстную информацию в исключении.
  2. Я использую общую попытку перехвата в точке входа в приложение или на самом высоком возможном уровне. Если возникает исключение, я просто регистрирую его и позволяю ему не работать. В идеале исключения никогда не должны попадать сюда.

Я считаю, что следующий код является запахом:

try
{
    //do something
}
catch(Exception)
{
   throw;
}

Такой код не имеет смысла и не должен включаться.


@ Джош, хороший момент о проглатывании исключений, но я считаю, что есть несколько случаев, когда допустимо просто проглатывать исключения. В последнем проекте ваш фрагмент кода был обязательным, от него воняло. Регистрация их всех усугубила ситуацию. Мой совет: если вы не можете обработать исключение, постарайтесь не проглотить его.
smaclell 03

Как я уже сказал, все зависит от приложения и конкретного контекста. Бывают случаи, когда я проглатываю исключения, хотя это бывает редко, и я не могу вспомнить, когда я это делал в последний раз ;-) Вероятно, это было, когда я писал свой собственный регистратор, и запись в журнал, и вторичный журнал терпели неудачу.
JoshBerke 03

Код служит точкой: вы можете установить точку останова на "throw".
Rauhotz 04

3
Слабое место: вы можете указать VS прерываться при возникновении любого исключения или сузить его и выбрать конкретное исключение. В VS2008 есть пункт меню под отладкой (вам нужно настроить панели инструментов, чтобы найти его) Вызываемые исключения
JoshBerke,

У примера "запах кода" есть определенный побочный эффект, даже в такой простой форме. Если // do somethingвключает какие-либо try/finallyблоки вокруг точки, которая выбрасывает, finallyблоки будут выполняться до catchблока. Без этого try/catchисключение будет перемещаться до вершины стека без выполнения каких-либо finallyблоков. Это позволяет обработчику верхнего уровня решать, выполнять ли finallyблоки или нет .
Дэниел Эрвикер

9

Я хотел бы порекомендовать еще один хороший источник по этой теме. Это интервью с изобретателями C # и Java, Андерсом Хейлсбергом и Джеймсом Гослингом, соответственно, на тему проверенного исключения Java.

Неудачи и исключения

Внизу страницы также есть отличные ресурсы.

Я склонен согласиться с Андерсом Хейлсбергом и вами в том, что большинство звонящих заботятся только о том, прошла ли операция успешно или нет.

Билл Веннерс : Вы упомянули о проблемах масштабируемости и версионности в отношении отмеченных исключений. Не могли бы вы пояснить, что вы имеете в виду под этими двумя проблемами?

Андерс Хейлсберг : Начнем с управления версиями, потому что проблемы там довольно легко увидеть. Скажем, я создаю метод foo, который объявляет, что он генерирует исключения A, B и C. Во второй версии foo я хочу добавить кучу функций, и теперь foo может генерировать исключение D. Это критическое изменение для меня. добавьте D в предложение throws этого метода, потому что существующий вызывающий этого метода почти наверняка не обработает это исключение.

Добавление нового исключения в предложение throws в новой версии приводит к нарушению клиентского кода. Это похоже на добавление метода в интерфейс. После публикации интерфейса он остается неизменным для всех практических целей, потому что в любой его реализации могут быть методы, которые вы хотите добавить в следующую версию. Поэтому вместо этого вам нужно создать новый интерфейс. Точно так же с исключениями вам придется либо создать совершенно новый метод с именем foo2, который генерирует больше исключений, либо вам придется перехватить исключение D в новом foo и преобразовать D в A, B или C.

Билл Веннерс : Но разве вы в любом случае не нарушаете их код, даже на языке без отмеченных исключений? Если новая версия foo будет генерировать новое исключение, которое клиенты должны подумать об обработке, не сломан ли их код только из-за того, что они не ожидали этого исключения при написании кода?

Андерс Хейлсберг : Нет, потому что во многих случаях людям все равно. Они не собираются обрабатывать ни одно из этих исключений. В их цикле сообщений есть обработчик исключений нижнего уровня. Этот обработчик просто вызовет диалоговое окно, в котором сообщается, что пошло не так, и продолжается. Программисты защищают свой код, написав повсюду try finally, поэтому они будут корректно отступать, если произойдет исключение, но на самом деле они не заинтересованы в обработке исключений.

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

РЕДАКТИРОВАТЬ: добавлены дополнительные сведения о преобразовании


Спасибо за это! Я обновил свой вопрос информацией о вашем ответе!
AtariPete, 03

Похоже, мистер Хейлсберг оправдывает обработку исключений Pokemon. Одна из огромных проблем с дизайном исключений в Java и C # заключается в том, что слишком много информации закодировано в типе исключения, в то время как информация, которая должна храниться в экземплярах исключений, недоступна каким-либо согласованным образом. Исключения должны распространяться вверх по стеку вызовов до тех пор, пока все представленные аномальные условия не будут разрешены; К сожалению, тип исключения - даже если он распознан - мало что говорит о том, разрешена ли ситуация. Если исключение нераспознано ...
supercat

... ситуация еще хуже. Если код вызывает FetchDataисключение неожиданного типа, он не может узнать, означает ли это исключение только то, что данные недоступны (в этом случае способность кода обойтись без этого "разрешит" его) или означает ли это, что ЦП горит и система должна выполнить «безопасное отключение» при первой возможности. Похоже, г-н Хейлсберг предполагает, что код должен предполагать первое; возможно, это лучшая стратегия с учетом существующих иерархий исключений, но, тем не менее, она кажется неприятной.
supercat 08

Я согласен, что throws Exceptin - это нелепо, потому что почти все может вызвать исключение, вы должны просто предположить, что это может произойти. Но когда вы указываете исключения, такие как throws A, B, C, это просто аннотация, для меня ее следует использовать как блок комментариев. Это все равно, что сказать: «Эй, клиент моей функции, вот совет, может быть, вы хотите разобраться с A, B, C, потому что эти ошибки могут произойти при использовании меня». Если вы добавите D в будущем, это не имеет большого значения, если с ним не будут работать, но это все равно, что добавить новую документацию, О, кстати, теперь также было бы полезно поймать D.
Дидье А.

8

Проверенные исключения - это спорный вопрос в целом и в Java в частности (позже я попытаюсь найти несколько примеров для тех, кто поддерживает и противостоит им).

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

  • Для удобства обслуживания всегда регистрируйте исключения, чтобы, когда вы начнете видеть ошибки, журнал поможет указать вам место, где, вероятно, возникла ошибка. Никогда не уходите printStackTrace()или тому подобное, есть вероятность, что один из ваших пользователей в конечном итоге получит одну из этих трассировок стека и точно не знает, что с ней делать.
  • Перехватывать исключения, которые вы можете обрабатывать, и только те, и обрабатывать их , а не просто бросать их в стек.
  • Всегда перехватывайте определенный класс исключений, и, как правило, никогда не следует ловить тип Exception, вы, скорее всего, проглотите важные исключения.
  • Никогда (никогда) не лови Errorс !! , что означает: никогда не ловите Throwables, поскольку Errors являются подклассами последнего. Errors проблемы, с которыми вы, скорее всего, никогда не сможете справиться (например OutOfMemory, или другие проблемы с JVM)

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


Проверенные исключения не являются проблемой в C #, потому что в нем их нет.
cletus 03

3
Imho, иногда полезно ловить ошибки: я вспоминаю написанное мною Java-приложение с очень интенсивным использованием памяти. Я поймал OutOfMemory-Ex и показал пользователю сообщение о том, что ему не хватает памяти, что он должен выйти из других программ, и сказал ему, как запустить jvm с выделенным дополнительным пространством кучи. Думаю, это помогло.
Лена Шиммель

5

Вы должны перехватывать только те исключения, с которыми можете иметь дело. Например, если вы имеете дело с чтением по сети, а время ожидания соединения истекает, и вы получаете исключение, вы можете попробовать еще раз. Однако, если вы читаете по сети и получаете исключение IndexOutOfBounds, вы действительно не можете справиться с этим, потому что вы не знаете (ну, в этом случае вы не знаете), что его вызвало. Если вы собираетесь вернуть false, -1 или null, убедитесь, что это для определенных исключений. Мне не нужна библиотека, которую я использую, возвращая ложь при чтении по сети, когда выбрасывается исключение, когда куча не в памяти.


3

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

Ex.

try
{
   sendMessage();

   if(message == success)
   {
       doStuff();
   }
   else if(message == failed)
   {
       throw;
   }
}
catch(Exception)
{
    logAndRecover();
}

Этот код заставляет меня нервничать. ИМО, вы не должны восстанавливаться после исключений, если это не критическая программа. Если вы выбрасываете исключения, то происходят плохие вещи.


2

Все вышеперечисленное кажется разумным, и часто на вашем рабочем месте может быть определенная политика. У нас мы определились с типами Exception: SystemException(не отмечено) и ApplicationException(отмечено).

Мы договорились, что SystemExceptionих вряд ли можно будет восстановить и что они будут обработаны один раз наверху. Чтобы обеспечить дополнительный контекст, наши SystemExceptions расширены, чтобы указать, где они произошли, например RepositoryException, ServiceEceptionи т. Д.

ApplicationExceptions может иметь бизнес-значение, как InsufficientFundsExceptionи должен обрабатываться клиентским кодом.

Вот конкретный пример, сложно комментировать вашу реализацию, но я бы никогда не использовал коды возврата, это проблема обслуживания. Вы можете проглотить исключение, но вам нужно решить, почему, и всегда регистрировать событие и трассировку стека. Наконец, поскольку ваш метод не имеет другой обработки, он довольно избыточен (кроме инкапсуляции?), Поэтому doactualStuffOnObject(p_jsonObject);может возвращать логическое значение!


1

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

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

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


1

Если вы собираетесь использовать шаблон кода в своем примере, назовите его TryDoSomething и перехватите только определенные исключения.

Также рассмотрите возможность использования фильтра исключений при регистрации исключений в диагностических целях. VB имеет языковую поддержку фильтров исключений. В ссылке на блог Греггма есть реализация, которую можно использовать с C #. Фильтры исключений имеют лучшие свойства для возможности отладки по сравнению с перехватом и повторным выбросом. В частности, вы можете зарегистрировать проблему в фильтре и позволить исключению продолжать распространяться. Этот метод позволяет подключить отладчик JIT (Just in Time) для получения полного исходного стека. Повторный бросок обрезает стопку в том месте, где она была повторно брошена.

Случаи, когда TryXXXX имеет смысл, - это когда вы обертываете стороннюю функцию, которая вызывает случаи, которые не являются действительно исключительными или которые просто сложно протестировать без вызова функции. Примером может быть что-то вроде:

// throws NumberNotHexidecimalException
int ParseHexidecimal(string numberToParse); 

bool TryParseHexidecimal(string numberToParse, out int parsedInt)
{
     try
     {
         parsedInt = ParseHexidecimal(numberToParse);
         return true;
     }
     catch(NumberNotHexidecimalException ex)
     {
         parsedInt = 0;
         return false;
     }
     catch(Exception ex)
     {
         // Implement the error policy for unexpected exceptions:
         // log a callstack, assert if a debugger is attached etc.
         LogRetailAssert(ex);
         // rethrow the exception
         // The downside is that a JIT debugger will have the next
         // line as the place that threw the exception, rather than
         // the original location further down the stack.
         throw;
         // A better practice is to use an exception filter here.
         // see the link to Exception Filter Inject above
         // http://code.msdn.microsoft.com/ExceptionFilterInjct
     }
}

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


Мне нравится шаблон TryXXX в .NET.
JoshBerke 03

1

Я предлагаю взять подсказки из стандартной библиотеки для используемого вами языка. Я не могу говорить за C #, но давайте посмотрим на Java.

Например, java.lang.reflect.Array имеет статический setметод:

static void set(Object array, int index, Object value);

Способ C был бы

static int set(Object array, int index, Object value);

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

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

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


Это очень правильный комментарий, уважайте язык и то, как он традиционно решает такие проблемы. Не привносите мышление C в мир Java.
AtariPete, 04

0

Если вы собираетесь перехватить Exception и вернуть false, это должно быть очень конкретное исключение. Вы этого не делаете, вы ловите их все и возвращаете false. Если я получу MyCarIsOnFireException, я хочу сразу об этом узнать! Остальные исключения меня могут не волновать. Таким образом, у вас должен быть стек обработчиков исключений, которые говорят «эй, эй, здесь что-то не так» для некоторых исключений (повторное генерирование или перехват и повторное генерирование нового исключения, которое лучше объясняет, что произошло) и просто возвращают false для других.

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

Изменить: Что касается вопроса об упаковке всего в try / catch, я думаю, что ответ - да. Исключения в вашем коде должны быть настолько редкими, что код в блоке catch выполняется так редко, что это вообще не влияет на производительность. Исключением должно быть состояние, когда ваш конечный автомат сломался и не знает, что делать. По крайней мере, повторно вызовите исключение, которое объясняет, что происходило в то время, и содержит исключение catch внутри него. «Исключение в методе doSomeStuff ()» не очень полезно для тех, кто должен выяснить, почему он сломался, пока вы в отпуске (или на новой работе).


Не забывайте, что установка блока исключений тоже стоит ...
devstuff

0

Моя стратегия:

Если исходная функция вернула void, я изменяю ее на возвращение bool . Если произошло исключение / ошибка, верните false , если все в порядке, верните true .

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

Вместо того , чтобы Ая строка может быть возвращена , содержащим описание ошибки.

В каждом случае перед возвратом чего-либо регистрируйте ошибку.


0

Здесь есть отличные ответы. Я хотел бы добавить, что если вы все-таки получите что-то вроде того, что вы опубликовали, по крайней мере, распечатайте больше, чем трассировку стека. Скажите, что вы делали в то время, и Ex.getMessage (), чтобы дать разработчику шанс побороться.


я полностью согласен. Я сделал только Ex.printStackTrace (); как пример того, что я делал в улове (т.е. не перебрасывал).
AtariPete, 03

0

Блоки try / catch образуют второй набор логики, встроенный в первый (основной) набор, поэтому они представляют собой отличный способ избавиться от нечитаемого и трудно поддающегося отладке кода спагетти.

Тем не менее, при разумном использовании они творит чудеса в удобочитаемости, но вы должны просто следовать двум простым правилам:

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

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

За исключением этих двух типов обработки ошибок, весь остальной код в середине должен быть свободен от кода try / catch и объектов ошибок. Таким образом, он работает просто и, как и ожидалось, независимо от того, где вы его используете или что вы с ним делаете.

Павел.


0

Возможно, я немного опоздал с ответом, но обработка ошибок - это то, что мы всегда можем изменить и развить со временем. Если вы хотите прочитать что-то еще по этой теме, я написал об этом сообщение в моем новом блоге. http://taoofdevelopment.wordpress.com

Удачного кодирования.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.