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

Возможный дубликат:
Почему обработка исключений неверна?

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

Существует ряд проблем, которые могут быть использованы для исключения. Почему нецелесообразно использовать их для потока управления? Какова философия, заключающаяся в том, что она исключительно консервативна в том, как они используются? Семантика? Представление? Сложность? Эстетика? Конвенция?

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

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

Ответ 1

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

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

Отвечая непосредственно на ваш вопрос. Исключения редко следует использовать, поскольку исключительные ситуации встречаются редко, а издержки дороги.

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

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

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

@Comments: Исключение можно определенно использовать в некоторых менее исключительных ситуациях, если это может сделать ваш код проще и проще. Этот вариант открыт, но я бы сказал, что на практике он встречается довольно редко.

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

Потому что исключения нарушают нормальный "поток управления". Вы поднимаете исключение, и нормальное выполнение программы заброшено, что потенциально оставляет объекты в противоречивом состоянии, а некоторые открытые ресурсы не готовы. Конечно, у С# есть оператор using, который гарантирует, что объект будет удален, даже если исключение выбрано из тела пользователя. Но давайте абстрактно на данный момент из языка. Предположим, что структура не будет размещать объекты для вас. Вы делаете это вручную. У вас есть система для запроса и освобождения ресурсов и памяти. У вас есть соглашение по всей системе, которое отвечает за освобождение объектов и ресурсов в каких ситуациях. У вас есть правила работы с внешними библиотеками. Он отлично работает, если программа соответствует нормальному операционному потоку. Но внезапно в середине исполнения вы бросаете исключение. Половина ресурсов остается неуважительной. Половина еще не была запрошена. Если операция должна была быть транзакционной, теперь она нарушена. Ваши правила обработки ресурсов не будут работать, потому что эти части кода, ответственные за освобождение ресурсов, просто не будут выполняться. Если кто-то еще захочет использовать эти ресурсы, они могут найти их в несогласованном состоянии и сбой, потому что они не могут предсказать эту конкретную ситуацию.

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

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

Ответ 2

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

  • Предварительные условия должны быть истинными перед вызовом функции.
  • Postcondition - это то, что функция гарантирует после того, как она вернется.
  • Безопасность исключений указывает, как исключения влияют на внутреннюю согласованность функции или структуры данных и часто имеют дело с поведением, переданным извне (например, functor, ctor параметра шаблона и т.д.).

Конструкторы

Здесь очень мало можно сказать о каждом конструкторе для каждого класса, который может быть написан на С++, но есть несколько вещей. Главным из них является то, что сконструированные объекты (т.е. Для которых конструктор успел вернуться) будут разрушены. Вы не можете изменить это постусловие, потому что язык предполагает, что это правда, и автоматически вызовет деструкторы. (Технически вы можете принять возможность поведения undefined, для которого язык не дает никаких гарантий ни о чем, но что вероятно, лучше охвачены в других местах.)

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

Пример зомби

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

string s = "abc";
if (s.memory_allocation_succeeded()) {
  do_something_with(s); // etc.
}

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

Проверка ввода примера

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

// boost::lexical_cast<int>() is the parsing function here
void show_square() {
  using namespace std;
  assert(cin); // precondition for show_square()
  cout << "Enter a number: ";
  string line;
  if (!getline(cin, line)) { // EOF on cin
    // error handling omitted, that EOF will not be reached is considered
    // part of the precondition for this function for the sake of example
    //
    // note: the below Python version throws an EOFError from raw_input
    //  in this case, and handling this situation is the only difference
    //  between the two
  }
  int n;
  try {
    n = boost::lexical_cast<int>(line);
    // lexical_cast returns an int
    // if line == "abc", it obviously cannot meet that postcondition
  }
  catch (boost::bad_lexical_cast&) {
    cout << "I can't do that, Dave.\n";
    return;
  }
  cout << n * n << '\n';
}

К сожалению, это показывает два примера того, как область С++ требует, чтобы вы нарушили RAII/SBRM. Пример в Python, который не имеет этой проблемы и показывает то, что я хочу, чтобы С++ имел – попробуй еще:

# int() is the parsing "function" here
def show_square():
  line = raw_input("Enter a number: ") # same precondition as above
  # however, here raw_input will throw an exception instead of us
  # using assert
  try:
    n = int(line)
  except ValueError:
    print "I can't do that, Dave."
  else:
    print n * n

Предпосылки

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

В частности, сравните ветки std:: logic_error и std:: runtime_error иерархии исключений stdlib. Первый часто используется для нарушений предварительных условий, в то время как последний более подходит для нарушений условий сообщения.

Ответ 3

  • Дорогостоящие
    вызовы ядра (или другие вызовы системного API) для управления сигнальными интерфейсами ядра (системы)
  • Трудно анализировать
    Многие проблемы оператора goto относятся к исключениям. Они часто переходят на потенциально большое количество кода в нескольких подпрограммах и исходных файлах. Это не всегда очевидно из чтения промежуточного исходного кода. (Это на Java.)
  • Не всегда ожидаемый промежуточным кодом
    Код, который перепрыгивает, может быть или не быть написан с возможностью исключения исключений. Если изначально так написано, возможно, это не поддерживалось с учетом этого. Подумайте: утечки памяти, утечки дескриптора файла, утечки сокетов, кто знает?
  • Устранение осложнений
    Сложнее поддерживать код, который перескакивает вокруг обработки исключений.

Ответ 4

Бросание исключения в некоторой степени похоже на инструкцию goto. Сделайте это для управления потоком, и вы закончите с непонятным кодом спагетти. Хуже того, в некоторых случаях вы даже не знаете, куда именно идет переход (т.е. Если вы не поймаете исключение в данном контексте). Это нагло нарушает принцип "наименее неожиданного", который повышает ремонтопригодность.

Ответ 5

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

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

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

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

Широкий консенсус по всем языкам, по-видимому, заключается в том, что это лучше всего сделать с точки зрения того, насколько вероятна ошибка при восстановлении (поскольку неустранимые ошибки не приводят к отсутствию кода с исключениями, но все же нуждаются в проверке и возврате -own-error в коде, который использует ошибки). Поэтому люди ожидают, что "эта функция, которую я вызываю, выдает исключение", означает "Я не могу продолжать", а не просто "она не может продолжаться". Это не присущее исключениям, это просто обычай, но, как любая хорошая практика программирования, это обычай, защищенный умными людьми, которые пробовали это по-другому и не пользовались результатами. У меня тоже были плохие переживания, бросающие слишком много исключений. Так что лично я думаю с точки зрения "действительно исключительного", если только что-то о ситуации делает исключение особенно привлекательным.

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

Ответ 6

На самом деле консенсуса нет. Вся проблема несколько субъективна, поскольку "целесообразность" исключения исключений часто предлагается существующими практиками в стандартной библиотеке самого языка. Стандартная библиотека С++ выдает исключения намного реже, чем стандартная библиотека Java, которая почти всегда предпочитает исключения, даже для ожидаемых ошибок, таких как неверный ввод пользователя (например, Scanner.nextInt). Это, я считаю, существенно влияет на мнения разработчиков о том, когда целесообразно исключать исключение.

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

Ответ 7

EDIT 11/20/2009:

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

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

Конечно, это только для .NET, а также специально для тех, кто разрабатывает высокопроизводительные приложения (например, я); так что это, очевидно, не универсальная правда. Тем не менее, разработчиков .NET очень много, поэтому я чувствовал, что стоит отметить.

ИЗМЕНИТЬ

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

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

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

Чтобы проиллюстрировать мою мысль, рассмотрим следующие примеры кода С#.

Пример 1: Обнаружение неверного ввода пользователя

Это пример того, что я бы назвал исключением abuse.

int value = -1;
string input = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        value = int.Parse(input);
        inputChecksOut = true;

    } catch (FormatException) {
        input = GetInput();
    }
}

Этот код для меня нелепо. Конечно, это работает. Никто не спорит с этим. Но это должно быть что-то вроде:

int value = -1;
string input = GetInput();

while (!int.TryParse(input, out value)) {
    input = GetInput();
}

Пример 2: Проверка наличия файла

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

string text = null;
string path = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        using (FileStream fs = new FileStream(path, FileMode.Open)) {
            using (StreamReader sr = new StreamReader(fs)) {
                text = sr.ReadToEnd();
            }
        }

        inputChecksOut = true;

    } catch (FileNotFoundException) {
        path = GetInput();
    }
}

Это кажется достаточно разумным, не так ли? Мы пытаемся открыть файл; если его нет, мы поймаем это исключение и попытаемся открыть другой файл... Что с этим не так?

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

string text = null;
string path = GetInput();

while (!File.Exists(path)) path = GetInput();

using (FileStream fs = new FileStream(path, FileMode.Open)) {
    using (StreamReader sr = new StreamReader(fs)) {
        text = sr.ReadToEnd();
    }
}

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

Использование блока try/catch: 25.455 секунд
Использование int.TryParse: 1.637 миллисекунды

Во втором примере я сделал в основном то же самое: сделал список из 10000 случайных строк, ни один из которых не был допустимым путем, а затем добавил допустимый путь на самый конец. Это были результаты:

Использование блока try/catch: 29.989 секунд
Использование File.Exists: 22.820 миллисекунды

Многие люди отреагировали на это, сказав: "Да, ну, бросая и ловя 10 000 исключений, крайне нереалистично, это преувеличивает результаты". Конечно. Разница между выбросом одного исключения и неправильным обращением к входу не будет заметна для пользователя. Факт остается фактом: использование этих исключений в этих двух случаях от 1000 до более чем в 10 000 раз медленнее, чем альтернативные подходы, которые так же читаемы - если не более того.

Вот почему я включил пример метода GetNine() ниже. Дело не в том, что оно невыносимо медленное или неприемлемо медленное; это, что он медленнее, чем это должно быть... без уважительной причины.

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


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

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

Вы также можете спросить: почему этот код плохо?

private int GetNine() {
    for (int i = 0; i < 10; i++) {
        if (i == 9) return i;
    }
}

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

То, что люди имеют в виду, когда говорят об исключении "злоупотребления".

Ответ 8

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

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

Если вы собираетесь использовать исключения, а затем:

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

С другой стороны, использование исключений в новых проектах с сильной командой может сделать код чище, проще в обслуживании и даже быстрее:

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

Ответ 9

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

Причина проста: исключения выходят из функции внезапно и распространяют стек на блок catch. Этот процесс очень дорого стоит вычислить: С++ создает свою систему исключений, чтобы иметь небольшие накладные расходы на "обычные" вызовы функций, поэтому, когда возникает исключение, он должен много работать, чтобы найти, куда идти. Более того, поскольку каждая строка кода может вызвать исключение. Если у нас есть некоторая функция f, которая часто вызывает исключения, мы должны позаботиться о том, чтобы использовать наши блоки try/catch вокруг каждого вызова f. Это довольно плохой интерфейс/реализация связи.

Ответ 10

Все эмпирические правила об исключениях сводятся к субъективным терминам. Вы не должны ожидать, чтобы получить жесткие и быстрые определения того, когда их использовать, а когда нет. "Только в исключительных обстоятельствах". Хорошее круговое определение: исключения относятся к исключительным обстоятельствам.

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

Есть много мнений и компромиссов. Найдите то, что говорит с вами, и следуйте за ним.

Ответ 11

Я упоминал этот вопрос в статье о исключениях на С++.

Соответствующая часть:

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

Ответ 12

Мой подход к обработке ошибок состоит в том, что существуют три основных типа ошибок:

  • Нечетная ситуация, которая может быть обработана на сайте ошибки. Это может быть, если пользователь вводит неверный ввод в командной строке. Правильное поведение - просто подать жалобу пользователю и цикл в этом случае. Другая ситуация может быть делением на ноль. Эти ситуации не являются действительно ситуациями с ошибками и обычно вызваны неправильным вводом.
  • Ситуация, подобная предыдущему виду, но та, которая не может быть обработана на сайте ошибки. Например, если у вас есть функция, которая принимает имя файла и анализирует файл с этим именем, возможно, он не сможет открыть файл. В этом случае он не может справиться с ошибкой. Это когда блестят исключения. Вместо использования подхода C (вернуть недопустимое значение в качестве флага и установить глобальную переменную ошибки, чтобы указать на проблему), код может вместо этого генерировать исключение. Затем вызывающий код сможет обработать исключение - например, для запроса пользователю другого имени файла.
  • Ситуация, которая не должна произойти. Это когда нарушается инвариант класса, или функция получает недействительный параметр или тому подобное. Это указывает на логический сбой в коде. В зависимости от уровня сбоя может быть целесообразным исключение, или принудительное немедленное прекращение может быть предпочтительным (как это делает assert). Как правило, эти ситуации указывают на то, что что-то сломалось где-то в коде, и вы фактически не можете доверять чему-то другому, чтобы быть верным - может быть необузданное повреждение памяти. Ваш корабль тонет, сойдите.

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

Ответ 13

Я прочитал некоторые ответы здесь. Я все еще удивляюсь, что это за путаница. Я категорически не согласен со всеми этими исключениями == код спагетти. С путаницей я имею в виду, что есть люди, которые не ценят обработку исключений С++. Я не уверен, как я узнал о обработке исключений в С++, но я понял последствия в течение нескольких минут. Это было около 1996 года, и я использовал компилятор borland С++ для OS/2. Мне никогда не приходилось решать, когда использовать исключения. Я обычно завершаю ошибочные действия do-undo в классы С++. Такие действия по отмене действия включают в себя:

  • создание/уничтожение дескриптора системы (для файлов, карт памяти, дескрипторов GUI WIN32, сокетов и т.д.)
  • установочные/снятые обработчики
  • выделение/освобождение памяти
  • утверждение/освобождение мьютекса
  • приращение/уменьшение количества ссылок
  • отображение/скрытие окна

Чем функциональные обертки. Чтобы обернуть системные вызовы (которые не попадают в прежнюю категорию) на С++. Например. чтение/запись из/в файл. Если что-то не получается, будет выведено исключение, которое содержит полную информацию об ошибке.

Затем есть исключения catch/rethrowing для добавления дополнительной информации об ошибке.

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

Можно объединить такие классы в сложные классы. Как только конструктор какого-либо элемента-элемента/объекта будет выведен, можно полагаться на то, что все остальные конструкторы одного и того же объекта (выполненные ранее) успешно выполнены.

Ответ 14

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

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

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

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

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

Ответ 15

В С++ есть несколько причин.

Во-первых, часто бывает трудно увидеть, откуда исходят исключения (поскольку они могут быть выброшены из почти чего-либо), и поэтому блок catch является чем-то вроде оператора COME FROM. Это хуже, чем GO TO, так как в GO TO вы знаете, откуда вы (утверждение, а не какой-то случайный вызов функции) и куда вы идете (ярлык). В основном это потенциально безопасная версия C setjmp() и longjmp(), и никто не хочет их использовать.

Во-вторых, С++ не содержит сборку мусора, поэтому классы С++, которые владеют ресурсами, избавляются от них в своих деструкторах. Поэтому в С++ исключение обработки системы должно запускать все деструкторы в области видимости. В языках с GC и никаких реальных конструкторов, таких как Java, исключение исключений намного менее обременительно.

В-третьих, сообщество С++, включая Bjarne Stroustrup и Комитет по стандартам и различные составители компиляторов, полагает, что исключения должны быть исключительными. В общем, это не стоит идти против языковой культуры. Реализации основаны на предположении, что исключения будут редкими. Лучшие книги рассматривают исключения как исключительные. Хороший исходный код использует несколько исключений. Хорошие разработчики С++ рассматривают исключения как исключительные. Чтобы пойти против этого, вам нужна хорошая причина, и все причины, которые я вижу, на стороне их исключительности.

Ответ 16

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

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   try {
      totalIncome= calculateIncomeAsTypeA();
   } catch (IncorrectIncomeTypeException& e) {
      totalIncome= calculateIncomeAsTypeB();
   }

   return totalIncome;
}

Это очень плохо, но вы должны писать:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   if (incomeType == A) {
      totalIncome= calculateIncomeAsTypeA();
   } else if (incomeType == B) {
      totalIncome= calculateIncomeAsTypeB();
   }
   return totalIncome;
}

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

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

Ответ 17

  • Поддержание работоспособности: как упоминалось выше, бросание исключений при падении шляпы сродни использованию gotos.
  • Взаимодействие: вы не можете взаимодействовать с библиотеками С++ с модулями C/Python (по крайней мере, не легко), если вы используете исключения.
  • Деградация производительности: RTTI используется для фактического поиска типа исключения, которое налагает дополнительные накладные расходы. Таким образом, исключения не подходят для обработки часто встречающихся случаев использования (пользователь вводил int вместо строки и т.д.).

Ответ 18

Я бы сказал, что исключения - это механизм, позволяющий вывести вас из текущего контекста (из текущего стекового кадра в простейшем смысле, но более того) безопасным способом. Это самое близкое структурированное программирование дошло до goto. Чтобы использовать исключения в том, как они были предназначены для использования, у вас должна быть ситуация, когда вы не можете продолжать то, что вы сейчас делаете, и вы не можете справиться с этим в том месте, где находитесь сейчас. Например, когда пароль пользователя неверен, вы можете продолжить, вернув false. Но если подсистема UI сообщает, что она даже не может запросить пользователя, просто возврат "login failed" был бы неправильным. Текущий уровень кода просто не знает, что делать. Поэтому он использует механизм исключения для делегирования ответственности кому-то выше, кто может знать, что делать.

Ответ 19

Одна очень практическая причина заключается в том, что при отладке программы я часто перехожу на First Chance Exceptions (Debug → Exceptions) для отладки приложения. Если есть много исключений, очень трудно найти, где что-то пошло "неправильно".

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

Ответ 20

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

Аргумент счетчика - это просто требует, чтобы ленивые люди вводили больше, чтобы стрелять себе в ногу.

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

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

Ответ 21

Законный случай для исключения:

  • Вы пытаетесь открыть файл, его нет, вызывается FileNotFoundException;

Незаконный случай:

  • Вы хотите что-то сделать, только если файл не существует, вы пытаетесь открыть файл, а затем добавить код в блок catch.

Я использую исключения, когда хочу разорвать поток приложения до определенной точки. Этот момент является местом catch (...) для этого исключения. Например, очень часто приходится обрабатывать нагрузку проектов, и каждый проект должен обрабатываться независимо от других. Таким образом, цикл, в котором обрабатываются проекты, имеет блок try... catch, и если во время обработки проекта выбрано какое-то исключение, все откатывается для этого проекта, регистрируется ошибка, а следующий проект обрабатывается. Жизнь продолжается.

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

RecordIterator<MyObject> ri = createRecordIterator();
try {
   MyObject myobject = ri.next();
} catch(NoSuchElement exception) {
   // Object doesn't exist, will create it
}

Это было бы лучше:

RecordIterator<MyObject> ri = createRecordIterator();
if (ri.hasNext()) {
   // It exists! 
   MyObject myobject = ri.next();
} else {
   // Object doesn't exist, will create it
}

КОММЕНТАРИЙ, ДОБАВЛЯЕМЫЙ К ОТВЕТУ:

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

Комментарии к этому могут добавить больше, чем мой ответ.

Ответ 22

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

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

Ответ 23

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

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

Я остановлюсь на другом:

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

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

Итак, вы закончите, элегантно сжигая свой процессор, не зная.

Итак: используйте исключения только в исключительных случаях - Значение: при возникновении реальных ошибок!

Ответ 24

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

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

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

Ответ 25

Я использую исключения, если:

  • произошла ошибка, которая не может быть восстановлена ​​с локального И
  • Если ошибка не восстанавливается, программа должна завершиться.

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

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

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

Ответ 26

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

Ответ 27

Мои два цента:

Мне нравится использовать исключения, потому что он позволяет мне программировать так, как будто ошибок не будет. Таким образом, мой код остается читаемым, а не разбросанным со всеми видами обработки ошибок. Конечно, обработка ошибок (обработка исключений) перемещается в конец (блок catch) или считается ответственностью вызывающего уровня.

Отличным примером для меня является обработка файлов или обработка баз данных. Предположим, что все в порядке, и закройте свой файл в конце или если возникло какое-то исключение. Или отмените транзакцию при возникновении исключения.

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

Для ParseInt, как упоминалось ранее, мне нравится идея исключений. Просто верните значение. Если параметр не обрабатывался, выведите исключение. Это делает ваш код чище, с одной стороны. На уровне вызова вам нужно сделать что-то вроде

try 
{
   b = ParseInt(some_read_string);
} 
catch (ParseIntException &e)
{
   // use some default value instead
   b = 0;
}

Код чист. Когда я получаю ParseInt, как это все разбросано, я создаю функции-обертки, которые обрабатывают исключения и возвращают значения по умолчанию. Например.

int ParseIntWithDefault(String stringToConvert, int default_value=0)
{
   int result = default_value;
   try
   {
     result = ParseInt(stringToConvert);
   }
   catch (ParseIntException &e) {}

   return result;
}

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

  • все еще нужно обрабатывать исключения. Дополнительная проблема: С++ не имеет синтаксиса, который позволяет указать, какие исключения может выполнять функция (например, java). Таким образом, уровень вызова не знает, какие исключения, возможно, потребуется обработать.
  • Иногда код может быть очень многословным, если каждая функция должна быть завершена в блок try/catch. Но иногда это все еще имеет смысл.

Таким образом, иногда трудно найти хороший баланс.

Ответ 28

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

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