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

29

Я использую исключения, чтобы поймать проблемы рано. Например:

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

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

Выдав исключение, если возникает эта проблема, я могу обнаружить и исправить ее, прежде чем она вызовет больше проблем в других местах программы. Исключение останавливает программу и сообщает мне, что «здесь произошло что-то плохое, исправьте это». Вместо этого nullперемещение по программе вызывает проблемы в других местах.

Теперь, вы правы, в этом случае nullэто просто вызвало бы NullPointerExceptionсразу, так что это может быть не лучшим примером.

Но рассмотрим метод, такой как этот, например:

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

В этом случае nullпараметр a будет передаваться по программе и может привести к ошибкам намного позже, что будет трудно отследить до их происхождения.

Меняем функцию так:

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

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

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


Поэтому мой вопрос прост: это хорошая практика? Хорошо ли мое использование исключений в качестве инструментов для предотвращения проблем? Это законное применение исключений или это проблематично?

Авив Кон
источник
2
Может ли downvoter объяснить, что не так с этим вопросом?
Джорджио
2
«ранний сбой, частый сбой»
Брайан Чен
16
Это буквально то, для чего существуют исключения.
raptortech97
Обратите внимание, что контракты на кодирование позволяют статически обнаруживать множество таких ошибок. Обычно это лучше, чем выбрасывать исключение во время выполнения. Поддержка и эффективность контрактов на кодирование зависит от языка.
Брайан
3
Поскольку вы генерируете IllegalArgumentException, излишне говорить « Входной человек».
Кевин Клайн

Ответы:

29

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

Килиан Фот
источник
Ранний сбой - это путь, если нет способа восстановиться после ошибки или состояния. Например, если вам нужно скопировать файл, а исходный или целевой путь пуст, вам следует сразу же сгенерировать исключение.
Михаил Шопсин
Это также известно как «провал быстро»
keuleJ
8

Да, выбрасывать исключения - хорошая идея. Брось их рано, бросай их часто, бросай их с нетерпением.

Я знаю, что есть дебаты «исключения против утверждений», с некоторыми видами исключительного поведения (в частности, те, которые, как считается, отражают ошибки программирования), обрабатываемые утверждениями, которые можно «компилировать» для времени выполнения, а не для отладки / тестирования сборок. Но объем производительности, потребляемой в нескольких дополнительных проверках правильности, минимален на современном оборудовании, и любые дополнительные расходы значительно перевешиваются ценностью получения правильных, не поврежденных результатов. Я на самом деле никогда не встречал кодовую базу приложения, для которой я хотел бы (большинство) проверок удалять во время выполнения.

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

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

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

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

Затем, когда кто-то видит PersonArgumentExceptionПонятно откуда. Существует балансирование относительно того, сколько пользовательских исключений вы хотите добавить, потому что вы не хотите без необходимости умножать сущности (Occam's Razor). Часто достаточно нескольких пользовательских исключений, чтобы сигнализировать «этот модуль не получает правильные данные!» или "этот модуль не может делать то, что должен!" таким образом, чтобы он был конкретным и адаптированным, но не настолько точным, чтобы вам пришлось заново реализовать всю иерархию исключений. Я часто прихожу к этому небольшому набору пользовательских исключений, начиная с фондовых исключений, затем сканируя код и осознавая, что «эти N мест порождают фондовые исключения, но они сводятся к идее более высокого уровня, что они не получают данные им нужно, пусть

Джонатан Юнис
источник
I've never actually met an application codebase for which I'd want (most) checks removed at runtime. Тогда вы не сделали код, который критичен к производительности. Сейчас я работаю над чем-то, что делает 37M операций в секунду с скомпилированными утверждениями и 42M без них. Утверждения не проверяют внешний ввод, они проверяют правильность кода. Мои клиенты более чем счастливы получить увеличение на 13%, как только я убедился, что мои вещи не сломаны.
Blrfl
Следует избегать перетаскивания кодовой базы с пользовательскими исключениями, потому что, хотя это может помочь вам, это не поможет другим, кто должен с ними познакомиться. Обычно, если вы обнаруживаете, что расширяете общее исключение и не добавляете каких-либо участников или функциональные возможности, вам это не нужно. Даже в вашем примере PersonArgumentExceptionэто не так ясно, как IllegalArgumentException. Последний общеизвестно, что его бросают, когда передается незаконный аргумент. Я бы на самом деле ожидал, что первый будет брошен, если a Personбыл в недопустимом состоянии для вызова (аналогично InvalidOperationExceptionC #).
Селали Адобор
Это правда, что если вы не будете осторожны, вы можете вызвать то же исключение из другого места в функции, но это недостаток кода, а не характера общих исключений. И вот тут-то и появляются отладочные и стековые трассировки. Если вы пытаетесь «пометить» ваши исключения, то используйте существующие средства, которые предоставляет большинство распространенных исключений, например, использование конструктора сообщений (именно для этого он и предназначен). Некоторые исключения даже предоставляют конструкторам дополнительные параметры для получения имени проблемного аргумента, но вы можете предоставить то же средство с правильно сформированным сообщением.
Селали Адобор
Я признаю, что простой ребрендинг общего исключения в частном лейбле в основном того же исключения является слабым примером и редко стоит усилий. На практике я пытаюсь создавать пользовательские исключения на более высоком семантическом уровне, более тесно связанные с целью модуля.
Джонатан Юнис
Я выполнял работу, чувствительную к производительности, в числовой сфере / области высокопроизводительных вычислений и ОС / промежуточного программного обеспечения. Увеличение производительности на 13% - это не маленькая вещь. Я мог бы отключить некоторые проверки, чтобы получить его, так же, как военный командир может запросить 105% от номинальной мощности реактора. Но я видел «это будет работать быстрее», часто приводимую как причина для отключения проверок, резервного копирования и других средств защиты. Это в основном компромисс между отказоустойчивостью и безопасностью (во многих случаях, включая целостность данных) для дополнительной производительности. Стоит ли это, это решение суда.
Джонатан Юнис
3

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

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

Например, в вашем конкретном случае: если какая-либо из ссылок имеет значение null, NullReferenceExceptionпри следующем утверждении будет выброшено значение при попытке определить возраст одного человека. Вам на самом деле не нужно проверять вещи здесь самостоятельно: пусть базовая система перехватывает эти ошибки и генерирует исключения, поэтому они существуют .

Для более реалистичного примера вы можете использовать assertоператоры, которые:

  1. Короче писать и читать:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. Специально разработаны для вашего подхода. В мире, где у вас есть как утверждения, так и исключения, вы можете выделить их следующим образом:

    • Утверждения относятся к ошибкам программирования («/ * это никогда не должно происходить * /»),
    • Исключения для угловых случаев (исключительные, но вероятные ситуации)

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

Статический анализатор (например, компилятор) тоже может быть счастливее.

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

CoreDump
источник
1

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

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

Я нахожу второе решение, например

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

тверже, потому что

  1. Он ловит ошибку как можно скорее, то есть когда registerPerson()вызывается, а не когда исключение пустого указателя генерируется где-то в стеке вызовов. Отладка становится намного проще: мы все знаем, как далеко может пройти недопустимое значение в коде, прежде чем оно проявится как ошибка.
  2. Это уменьшает связь между функциями: registerPerson()не делает никаких предположений о том, какие другие функции в конечном итоге будут использовать personаргумент и как они будут его использовать: решение, которое nullявляется ошибкой, принимается и реализуется локально.

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

Джорджио
источник
1
Указание причины («Вводимые данные являются нулевыми») также помогает понять проблему, в отличие от того, когда какой-то непонятный метод в библиотеке дает сбой.
Флориан Ф
1

В общем, да, это хорошая идея "рано провалиться". Однако в вашем конкретном примере явное IllegalArgumentExceptionне дает существенного улучшения по сравнению с NullReferenceException- поскольку оба оперируемых объекта уже передаются в качестве аргументов функции.

Но давайте посмотрим на немного другой пример.

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

Если бы в конструкторе не было проверки аргументов, вы бы получили NullReferenceExceptionпри вызове Calculate.

Но сломанный кусок кода не был ни Calculateфункцией, ни потребителем Calculateфункции. Разбитым фрагментом кода был код, который пытается PersonCalculatorсоздать нулевое значение, Personпоэтому мы хотим, чтобы исключение происходило.

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

Пит
источник
0

Не в приведенных вами примерах.

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

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

Но в общем, вы должны бросить рано, как только вы определите, что что-то пошло не так (при соблюдении DRY). Это, возможно, не очень хорошие примеры этого.

Telastyn
источник
-3

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

С одной стороны, в любом случае нет смысла пропускать ноль, поэтому я сразу выясню проблему, основываясь на метании a NullReferenceException.

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

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

как зовут
источник
Я работал в течение нескольких лет на основе кода, основанного на этом принципе: указатели просто снимаются ссылками при необходимости и почти никогда не проверяются на NULL и обрабатываются явно. Отладка этого кода всегда была очень трудоемкой и хлопотной, так как исключения (или дампы ядра) происходят намного позже, когда возникает логическая ошибка. Я потратил буквально недели на поиски определенных ошибок. По этой причине я предпочитаю стиль «как можно быстрее», по крайней мере, для сложного кода, где не сразу очевидно, откуда возникает исключение нулевого указателя.
Джорджио
«С одной стороны, в любом случае не имеет смысла пропускать нулевое значение, поэтому я сразу выясню проблему, основываясь на исключении NullReferenceException.»: Не обязательно, как иллюстрирует второй пример Prog.
Джорджио
«Во-вторых, если каждая функция выдает немного разные исключения в зависимости от того, какие явно вводимые данные были предоставлены, то довольно скоро вы получите функции, которые могут генерировать 18 различных типов исключений ...»: в этом случае вы может использовать одно и то же исключение (даже NullPointerException) во всех функциях: важно бросать раньше, а не бросать разные исключения из каждой функции.
Джорджио
Вот для чего нужны RuntimeExceptions. Любое условие, которое «не должно происходить», но мешает работе вашего кода, должно вызывать его. Вам не нужно объявлять их в сигнатуре метода. Но вам нужно поймать их в какой-то момент и сообщить, что функция запрашиваемое действие не удалась.
Флориан F
1
Вопрос не определяет язык, поэтому полагаться, например RuntimeException, не обязательно на правильное предположение.