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

193

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

C # и Java (и слишком много других) имеют множество типов поведения «переполнения», которое мне совсем не нравится (например, type.MaxValue + type.SmallestValue == type.MinValueнапример:) int.MaxValue + 1 == int.MinValue.

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

Переопределенный Addметод:

/// <summary>
/// Increments this date with a timespan, but loops when
/// the maximum value for datetime is exceeded.
/// </summary>
/// <param name="ts">The timespan to (try to) add</param>
/// <returns>The Date, incremented with the given timespan. 
/// If DateTime.MaxValue is exceeded, the sum wil 'overflow' and 
/// continue from DateTime.MinValue. 
/// </returns>
public DateTime override Add(TimeSpan ts) 
{
    try
    {                
        return base.Add(ts);
    }
    catch (ArgumentOutOfRangeException nb)
    {
        // calculate how much the MaxValue is exceeded
        // regular program flow
        TimeSpan saldo = ts - (base.MaxValue - this);
        return DateTime.MinValue.Add(saldo)                         
    }
    catch(Exception anyOther) 
    {
        // 'real' exception handling.
    }
}

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

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

ИМХО, реакция «Никогда не используйте их для регулярного выполнения программы», как кажется, у всех, похоже, не так уж недооценена, как может оправдать сила этой реакции.

Или я ошибаюсь?

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

  1. Очистить
  2. Уважайте контракт вашего метода

Стреляй в меня.

Питер
источник
2
+1 Я чувствую то же самое. Помимо производительности, единственная веская причина избежать потока исключений для управления - это когда код вызывающей стороны будет намного более читаемым с возвращаемыми значениями.
4
это: вернуть -1, если что-то произошло, вернуть -2, если что-то еще, и т.д ... действительно более читабельным, чем исключения?
Кендер,
1
Грустно, что человек получает негативную репутацию за то, что говорит правду: что ваш пример не мог быть написан с помощью утверждений if. (Нельзя сказать, что это правильно / завершено.)
Инго
8
Я бы сказал, что иногда исключение может быть единственным вариантом. Например, у меня есть бизнес-компонент, который инициализирует свое внутреннее состояние в конструкторе, опрашивая базу данных. Бывают случаи, когда в базе данных нет соответствующих данных. Бросок исключения в конструкторе - единственный способ эффективно отменить конструкцию объекта. Это четко указано в контракте (Javadoc в моем случае) класса, поэтому у меня нет проблем, чтобы клиентский код мог (и должен) перехватить это исключение при создании компонента и продолжить с него.
Стефан Хаберл
1
Поскольку вы сформулировали гипотезу, вы должны также привести подтверждающие доказательства / причины. Для начала назовите одну из причин, по которой ваш код превосходит гораздо более короткое, самодокументированное ifутверждение. Вы найдете это очень сложно. Другими словами: сама ваша предпосылка ошибочна, и выводы, которые вы из нее делаете, ошибочны.
Конрад Рудольф

Ответы:

171

Вы когда-нибудь пытались отладить программу, генерирующую пять исключений в секунду в ходе обычной работы?

У меня есть.

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

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

Моя точка зрения: если вы используете исключения для нормальных ситуаций, как вы находите необычные (то есть исключения ) ситуации?

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

Бранн
источник
13
Пример: когда я отлаживаю программу .net, я запускаю ее из visual studio и прошу VS сломать все исключения. Если вы полагаетесь на исключения как на ожидаемое поведение, я больше не могу этого делать (поскольку он будет прерываться 5 раз в секунду), и гораздо сложнее найти проблемную часть кода.
Бранн
15
+1 за указание на то, что вы не хотите создавать стог сена для исключения, в котором можно найти настоящую исключительную стрелку
Грант Вагнер
16
я не получаю этот ответ вообще, я думаю, что люди не понимают здесь. Это не имеет ничего общего с отладкой вообще, но с дизайном. Боюсь, это круговое рассуждение в чистом виде. И ваша точка зрения действительно помимо вопроса, подобного изложенному ранее
Петр,
11
@Peter: отладка без нарушения исключений трудна, и перехват всех исключений является болезненным, если их много в дизайне. Я думаю, что дизайн, который усложняет отладку, почти частично сломан (другими словами, дизайн имеет какое-то отношение к отладке, IMO)
Brann
7
Даже игнорируя тот факт, что большинство ситуаций, которые я хочу отладить, не соответствуют генерируемым исключениям, ответ на ваш вопрос: «по типу», например, я скажу отладчику перехватывать только AssertionError или StandardError или что-то, что делает соответствуют происходящим плохим вещам. Если у вас есть проблемы с этим, то как вы ведете логи - не ведете ли вы логи по уровню и классу, именно так, чтобы вы могли фильтровать их? Думаешь, это тоже плохая идея?
Кен
158

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

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

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

Антон Гоголев
источник
11
эй, исключения не удивительны! И вы как бы противоречите себе, когда говорите «это плохая идея», а затем продолжаете говорить «но это хорошая идея в python».
hasen
6
Я до сих пор совсем не убежден: 1) эффективность была помимо вопроса, многим программам, не относящимся к делу, не все равно (например, пользовательский интерфейс). остается вопрос: почему бы не использовать идентификатор в первую очередь? Но, так как это ответ
Питер
4
+1 На самом деле я рад, что вы указали на разницу между Python и C #. Я не думаю, что это противоречие. Python гораздо более динамичен, и ожидание использования исключений таким образом встроено в язык. Это также часть культуры EAFP в Python. Я не знаю, какой подход является концептуально более чистым или более самосогласованным, но мне нравится идея написания кода, который выполняет то, что ожидают другие , что означает разные стили на разных языках.
Марк Э. Хаазе
13
В отличие от goto, конечно, исключения правильно взаимодействуют с вашим стеком вызовов и с лексической областью видимости и не оставляют стек или области в беспорядке.
Лукас Эдер
4
На самом деле большинство поставщиков виртуальных машин ожидают исключения и эффективно их обрабатывают. Как отмечает @LukasEder, исключения полностью отличаются от goto тем, что они структурированы.
Марчин
30

Мое правило:

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

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

Возьмите этот пример:

try
{
   DoSomeMethod();  //Can throw Exception1
   DoSomeOtherMethod();  //Can throw Exception1 and Exception2
}
catch(Exception1)
{
   //Okay something messed up, but is it SomeMethod or SomeOtherMethod?
}

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

Class1 myInstance;
try
{
   myInstance = Class1Factory.Build();
}
catch(SomeException)
{
   // Couldn't instantiate class, do something else..
}
myInstance.BestMethodEver();   // Will throw a compile-time error, saying that myInstance is uninitalized, which it potentially is.. :(

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

Надеюсь, я правильно понял ваш вопрос :)

cwap
источник
4
Re: Ваш второй пример - почему бы просто не поместить вызов BestMethodEver внутри блока try после Build? Если Build () выдает исключение, оно не будет выполнено, и компилятор доволен.
Blorgbeard отсутствует
2
Да, это, вероятно, будет то, что вы в конечном итоге, но рассмотрим более сложный пример, где сам тип myInstance может генерировать исключения .. И другие isntances в области видимости метода тоже могут. Вы получите множество вложенных блоков try / catch :(
cwap
Вы должны выполнить преобразование Exception (в тип Exception, соответствующий уровню абстракции) в вашем блоке catch. К вашему сведению: «Multi-catch» должен войти в Java 7.
jasonnerothin
К вашему сведению: в C ++ вы можете поставить несколько перехватов после попытки перехватить разные исключения.
RobH
2
Для программного обеспечения термоусадочной пленки, вы должны перехватить все исключения. По крайней мере, откройте диалоговое окно, которое объясняет, что программа должна быть закрыта, и вот что-то непонятное, что вы можете отправить в отчете об ошибке.
Дэвид Торнли
25

Первая реакция на множество ответов:

ты пишешь для программистов и принцип наименьшего удивления

Конечно! Но если просто не все яснее все время.

Не должно быть удивительно, например: catch (1 / x) catch (DivisionByZero) более понятен, чем любой, если мне (в Конраде и других). Тот факт, что такого рода программирование не ожидается, является чисто условным и действительно актуальным. Может быть, в моем примере, если было бы яснее.

Но DivisionByZero и FileNotFound в этом отношении понятнее, чем ifs.

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

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

исключения для нормальных ситуаций, как вы находите необычные (то есть исключительные) ситуации?

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

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

Питер
источник
1
1) Вы всегда проверяете xперед звонком 1/x? 2) Оборачиваете ли вы каждую операцию деления в блок try-catch для отлова DivideByZeroException? 3) Какую логику вы положили в блок catch для восстановления DivideByZeroException?
Лайтман
1
За исключением DivisionByZero и FileNotFound - плохие примеры, поскольку они являются исключительными случаями, которые следует рассматривать как исключения.
0x6C38
Нет ничего «чрезмерно исключительного» в том, что файл не найден таким образом, как говорят «анти-исключения». openConfigFile();может сопровождаться перехваченным FileNotFound с { createDefaultConfigFile(); setFirstAppRun(); }исключением FileNotFound, обработанным изящно; без сбоев, давайте сделаем опыт конечного пользователя лучше, а не хуже. Вы можете сказать: «Но что, если это был не первый запуск, и они получают это каждый раз?» По крайней мере, приложение запускается каждый раз и не вылетает при каждом запуске! На 1: 10 «это ужасно»: «первый запуск» при каждом запуске = 3 или 4, сбой при каждом запуске = 10.
Loduwijk
Ваши примеры являются исключениями. Нет, вы не всегда проверяете xперед звонком 1/x, потому что обычно все в порядке. Исключительный случай - случай, когда это не хорошо. Мы здесь не говорим о том, как разбить землю, но, например, для простого целого числа, заданного случайным образом x, только 1 в 4294967296 не сможет выполнить деление. Это исключение, и исключения - хороший способ справиться с этим. Тем не менее, вы можете использовать исключения для реализации эквивалентого switchзаявления, но это было бы довольно глупо.
Thor84no
16

До исключений, в C было setjmpи longjmpможет быть использовано для выполнения аналогичной развертки фрейма стека.

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

Исключения немного более общие в том смысле, что вы можете использовать их в одном и том же фрейме стека. Это поднимает аналогии с тем, gotoчто я считаю неправильным. Gotos - это тесно связанная пара (а также setjmpи longjmp). Исключения следуют из слабосвязанной публикации / подписки, которая намного чище! Поэтому их использование в одном и том же стековом кадре вряд ли то же самое, что использованиеgoto s.

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

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

Мое любимое использование - это последовательность throw new Success()длинного фрагмента кода, который пробует одно за другим, пока не найдет то, что ищет. Каждая вещь - каждая часть логики - может иметь произвольную вложенность, поэтому breakее нет, как и любой вид условных тестов. if-elseКартина ломкая. Если я отредактируюelse или испорчу синтаксис каким-либо другим способом, то это будет волосатая ошибка.

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

Иногда мой код проверяет одну вещь за другой и только успешно, если все в порядке. В этом случае у меня есть аналогичная линеаризация с использованиемthrow new Failure() .

Использование отдельной функции портит естественный уровень компартментализации. Так чтоreturn решение не является оптимальным. Я предпочитаю иметь одну или две страницы кода в одном месте по познавательным причинам. Я не верю в ультра тонко разделенный код.

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

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

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

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

некромант
источник
3
Используя этот шаблон, у большинства моих грубых функций в конце есть две маленькие зацепки - одна для Успеха и одна для Отказа, и именно здесь функция оборачивает вещи, такие как подготовка правильного ответа сервлета или подготовка возвращаемых значений. Наличие единого места для подведения итогов - это хорошо. return-Pattern альтернативы может потребоваться две функции для каждой такой функции. Внешний - для подготовки ответа сервлета или других подобных действий, а внутренний - для вычисления. PS: Английский профессор, вероятно, предложил бы использовать «удивительный», а не «удивительный» в последнем абзаце :-)
некромант
11

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

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

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

mouviciel
источник
Думаю, недооцененный ответ. Компьютер может не сильно тормозить, когда обнаруживает, что исключение проглатывается, но когда я работаю над чьим-то кодом и сталкиваюсь с ним, он останавливает меня на месте, пока я работаю, если пропустил что-то важное, чего не сделал. Не знаю, или нет ли оправдания для использования этого анти-паттерна.
Тим Абелл
9

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

Джеймс Кох
источник
8

Джош Блох активно занимается этой темой в «Эффективной Java». Его предложения освещают и должны относиться также к .Net (за исключением деталей).

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

Например, второй метод проще в использовании, чем первый:

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 * @throws AdditionException if addend1 or addend2 is less than or equal to zero
 */
int addPositiveNumbers(int addend1, int addend2) throws AdditionException{
  if( addend1 <= 0 ){
     throw new AdditionException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new AdditionException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 */
public int addPositiveNumbers(int addend1, int addend2) {
  if( addend1 <= 0 ){
     throw new IllegalArgumentException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new IllegalArgumentException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

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

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

Основным моментом является то, что исключения не должны использоваться в качестве кодов возврата, в основном потому, что вы усложнили не только ВАШ API, но и API вызывающего.

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

jasonnerothin
источник
7

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

Лучший способ сократить затраты на создание исключений - переопределить метод fillInStackTrace () следующим образом:

public Throwable fillInStackTrace() { return this; }

Такое исключение не будет заполнено.

paweloque
источник
Трассировка стека также требует, чтобы вызывающая сторона «знала» (т.е. имела зависимость от) все Throwables в стеке. Это плохо. Бросать исключения, соответствующие уровню абстракции (ServiceExceptions в службах, DaoException в методах Dao и т. Д.). Просто переведите, если необходимо.
jasonnerothin
5

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

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

Джейсон Пуньон
источник
5

Вот лучшие практики, которые я описал в своем блоге :

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

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

Gambrinus
источник
4

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

Например, когда я пытаюсь отследить ошибку в VS, я обычно включаю «разбить на все исключения». Если вы используете исключения для управления потоком, то я буду регулярно ломать отладчик и буду вынужден игнорировать эти неисключительные исключения, пока не доберусь до реальной проблемы. Это может свести с ума кого-то !!

Шон
источник
1
Я уже говорил о том, что лучше: отладка всех исключений: то же самое, это просто сделано, потому что дизайн, не использующий исключения, является распространенным явлением. Мой вопрос был: почему это вообще распространено?
Питер
Итак, ваш ответ в основном "Это плохо, потому что Visual Studio имеет эту единственную особенность ..."? Я занимаюсь программированием около 20 лет, и я даже не заметил, что была опция «перерыв на все исключения». Тем не менее, «из-за этой особенности!» звучит как слабая причина. Просто проследите исключение до его источника; надеюсь, вы используете язык, который облегчает эту задачу - иначе ваша проблема связана с особенностями языка, а не с общим использованием самих исключений.
Лодувийк
3

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

Используя возвращаемые значения для сообщения об ошибке проверки, все просто: если метод вернул число, меньшее 0, произошла ошибка. Как тогда сказать, какой параметр не прошел проверку?

Я помню, что из моих дней C многие функции возвращали коды ошибок, например:

-1 - x lesser then MinX
-2 - x greater then MaxX
-3 - y lesser then MinY

и т.п.

Это действительно менее читабельно, чем бросать и ловить исключение?

кендер
источник
вот почему они изобрели перечисления :) Но магические числа - это совсем другая тема .. en.wikipedia.org/wiki/…
Исак Саво,
Отличный пример. Я собирался написать то же самое. @IsakSavo: перечисления не помогают в этой ситуации, если ожидается, что метод вернет какое-то значение или объект. Например, getAccountBalance () должен возвращать объект Money, а не объект AccountBalanceResultEnum. Многие программы на С имеют похожую схему, в которой одно значение часового (0 или ноль) представляет ошибку, а затем вам нужно вызвать другую функцию, чтобы получить отдельный код ошибки, чтобы определить причину возникновения ошибки. (MySQL C API похож на это.)
Марк Э. Хаас
3

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

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

Сказал по-другому: просто потому, что вы можете , не значит, что вы должны .

Брайан Уоттс
источник
1
Вы говорите, что нет необходимости в исключении, после того, как мы получили ifнормальное использование или использование исключений не предназначено, потому что это не предназначено (круговой аргумент)?
Val
1
@Val: Исключения предназначены для исключительных ситуаций - если мы обнаружим достаточно, чтобы сгенерировать исключение и обработать его, у нас будет достаточно информации, чтобы не выбрасывать его и обрабатывать. Мы можем перейти непосредственно к логике обработки и пропустить дорогостоящую, лишнюю попытку / отлов.
Брайан Уоттс
По этой логике вы также можете не иметь исключений и всегда выполнять системный выход вместо того, чтобы генерировать исключение. Если вы хотите что-то сделать перед выходом, сделайте обертку и назовите это. Пример Java: public class ExitHelper{ public static void cleanExit() { cleanup(); System.exit(1); } }затем просто вызовите это вместо броска: ExitHelper.cleanExit();если бы ваш аргумент был надежным, то это был бы предпочтительный подход, и не было бы никаких исключений. Вы в основном говорите: «Единственная причина исключений - сбой другим способом».
Лодувийк
@ Аарон: Если я скину и поймаю исключение, у меня будет достаточно информации, чтобы избежать этого. Это не значит, что все исключения внезапно заканчиваются смертельным исходом. Другой код, который я не контролирую, может его поймать, и это нормально. Мой аргумент, который остается обоснованным, заключается в том, что выбрасывать и перехватывать исключение в одном и том же контексте является излишним. Я не заявлял и не заявлял, что все исключения должны выходить из процесса.
Брайан Уоттс
@BryanWatts Подтверждено. Многие другие уже сказали , что вы должны использовать только исключения для чего вы не можете оправиться от, и расширения всегда должен быть сбой на исключениях. Вот почему трудно обсуждать эти вещи; Есть не только 2 мнения, но и много. Я все еще не согласен с вами, но не сильно. Бывают моменты, когда команды throw / catch вместе представляют собой наиболее читаемый, поддерживаемый и приятный на вид код; обычно это происходит, если вы уже перехватываете другие исключения, поэтому у вас уже есть try / catch и добавление еще 1 или 2 перехватов чище, чем отдельные проверки ошибок if.
Лодувийк
3

Как уже неоднократно упоминали другие, принцип наименьшего удивления запрещает чрезмерное использование исключений только для целей потока управления. С другой стороны, ни одно правило не является на 100% правильным, и всегда есть случаи, когда исключением является «просто правильный инструмент» - gotoкстати, очень похожий на самого себя, который поставляется в форме breakиcontinue на таких языках, как Java, что часто являются идеальным способом выпрыгнуть из сильно вложенных циклов, которых не всегда можно избежать.

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

Это объясняет, как внутри jOOQ (библиотека абстракции SQL для Java) такие исключения иногда используются для прерывания процесса рендеринга SQL на ранних этапах, когда выполняется какое-то «редкое» условие.

Примеры таких условий:

  • Слишком много значений связывания. Некоторые базы данных не поддерживают произвольное количество значений связывания в своих операторах SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). В этих случаях jOOQ прерывает фазу рендеринга SQL и повторно отображает инструкцию SQL со встроенными значениями связывания. Пример:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }

    Если бы мы явно извлекали значения связывания из запроса AST для их подсчета каждый раз, мы бы тратили ценные циклы ЦП на те 99,9% запросов, которые не страдают от этой проблемы.

  • Некоторая логика доступна только косвенно через API, который мы хотим выполнить только «частично». UpdatableRecord.store()Метод генерирует INSERTили UPDATEзаявление, в зависимости от Recordвнутренних флагов «S. Со стороны мы не знаем, в какой логике содержится store()(например, оптимистическая блокировка, обработка прослушивателя событий и т. Д.), Поэтому мы не хотим повторять эту логику, когда мы храним несколько записей в пакетном операторе, где мы хотели бы store()только генерировать оператор SQL, а не выполнять его на самом деле. Пример:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }

    Если бы мы внедрили store()логику в «повторно используемый» API, который можно настроить так, чтобы он не выполнял SQL, мы бы искали создание довольно сложного в обслуживании, едва ли повторно используемого API.

Вывод

По сути, мы используем эти нелокальные gotos только в соответствии с тем, что [Mason Wheeler] [5] сказал в своем ответе:

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

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

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

Лукас Эдер
источник
2

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

В общем, если вы знаете, что существует большая вероятность сбоя, который вы можете проверить ... вы должны выполнить проверку ... т.е. если (obj! = Null) obj.method ()

В вашем случае я недостаточно знаком с библиотекой C #, чтобы знать, есть ли у даты и времени простой способ проверить, не вышла ли временная метка за пределы. Если это так, просто вызовите if (.isvalid (ts)), иначе ваш код в основном в порядке.

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


источник
Добавление: если ваше исключение предоставляет информацию о сбое при сбое (метод получения, например, «Param getWhatParamMessedMeUp ()»), это может помочь пользователю вашего API принять правильное решение о том, что делать дальше. В противном случае вы просто даете имя состоянию ошибки.
jasonnerothin
2

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

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

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

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

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

Наконец, предварительные проверки показывают, что вы знаете или ожидаете, и делают это явным. Код должен быть понятен в намерениях. Что бы вы предпочли прочитать?


источник
1
Совсем не правда: «По сути, вы используете исключение для оператора else, если вы используете его для потока управления.» Если вы используете его для потока управления, вы точно знаете, что вы ловите, и никогда не используете общий улов, но конкретный конечно!
Питер
2

Возможно, вам будет интересно взглянуть на систему условий Common Lisp, которая является своего рода обобщением исключений, сделанных правильно. Поскольку вы можете разматывать стек или не контролируемым образом, вы также получаете «перезапуск», что очень удобно.

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

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

Саймон
источник
2

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

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

Пример: Microsoft Volta - это проект, позволяющий писать веб-приложения в прямом .NET и позволяющий платформе позаботиться о том, какие биты должны выполняться и где. Одним из следствий этого является то, что Volta должна иметь возможность компилировать CIL в JavaScript, чтобы вы могли запускать код на клиенте. Однако есть проблема: .NET имеет многопоточность, а JavaScript - нет. Итак, Volta реализует продолжения в JavaScript с использованием исключений JavaScript, а затем реализует потоки .NET, используя эти продолжения. Таким образом, приложения Volta, использующие потоки, могут быть скомпилированы для запуска в неизмененном браузере - Silverlight не требуется.

Йорг Миттаг
источник
2

Одна эстетическая причина:

Попытка всегда идет с уловом, тогда как если не нужно идти с другим.

if (PerformCheckSucceeded())
   DoSomething();

С try / catch это становится намного более многословным.

try
{
   PerformCheckSucceeded();
   DoSomething();
}
catch
{
}

Это 6 строк кода слишком много.

Гзак
источник
1

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

меш
источник
1

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

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

Инго
источник
Извини, это совершенно неправильно, Бранн. Это зависит от состояния. Это не всегда тот случай, когда условие тривиально. Таким образом, утверждение if может занять несколько часов, дней или даже больше.
Инго
В JVM это так. Не дороже, чем возврат. Пойди разберись. Но вопрос в том, что бы вы написали в операторе if, если не в самом коде, который уже присутствует в вызываемой функции, чтобы отличить исключительный случай от обычного - таким образом, дублирование кода.
Инго
1
Инго: исключительная ситуация - та, которую ты не ожидаешь. то есть тот, о котором вы не думали как программист. Так что мое правило - «писать код, который не генерирует исключения» :)
Бранн,
1
Я никогда не пишу обработчики исключений, я всегда исправляю проблему (за исключением случаев, когда я не могу этого сделать, потому что у меня нет контроля над ошибочным кодом). И я никогда не выкидываю исключения, кроме случаев, когда написанный мной код предназначен для использования кем-то другим (например, библиотекой). Не показывать мне противоречие?
Бранн
1
Я согласен с вами не бросать исключения дико. Но, конечно, что является «исключительным», это вопрос определения. Это String.parseDouble выдает исключение, если он не может доставить полезный результат, например, в порядке. Что еще это должно сделать? Вернуть NaN? Как насчет не IEEE оборудования?
Инго
1

Существует несколько общих механизмов, с помощью которых язык может позволить методу завершать работу, не возвращая значения, и возвращаться к следующему блоку «catch»:

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

  • Пусть метод вернет «скрытый» флаг, который отличает нормальный возврат от исключения, и пусть вызывающий проверит этот флаг и ответвление на процедуру «исключение», если она установлена. Эта подпрограмма добавляет 1-2 инструкции к случаю без исключения, но относительно небольшую нагрузку при возникновении исключения.

  • Пусть вызывающий абонент разместит информацию или код обработки исключений по фиксированному адресу относительно сложенного обратного адреса. Например, с ARM вместо использования инструкции «BL-подпрограмма» можно использовать последовательность:

        adr lr,next_instr
        b subroutine
        b handle_exception
    next_instr:
    

Чтобы выйти нормально, подпрограмма просто сделала бы bx lrили pop {pc}; в случае ненормального выхода подпрограмма должна либо вычесть 4 из LR перед выполнением возврата или использования sub lr,#4,pc(в зависимости от вариации ARM, режима выполнения и т. д.). Этот подход будет работать очень плохо, если вызывающая сторона не предназначена для его размещения.

Язык или инфраструктура, в которых используются проверенные исключения, могут выиграть от того, что они обрабатываются с помощью механизма, подобного # 2 или # 3 выше, в то время как непроверенные исключения обрабатываются с использованием # 1. Хотя реализация проверенных исключений в Java довольно неприятна, они не были бы плохой концепцией, если бы существовал способ, с помощью которого сайт вызова мог бы сказать, по сути, «Этот метод объявлен как выбрасывающий XX, но я не ожидаю этого когда-нибудь сделать это, если это произойдет, перебросить как «непроверенное» исключение. В среде, где проверенные исключения обрабатывались таким образом, они могут быть эффективным средством управления потоком для таких вещей, как методы синтаксического анализа, которые в некоторых контекстах могут иметь высокая вероятность неудачи, но там, где неудача должна возвращать информацию, принципиально отличную от успеха. Однако я не знаю каких-либо фреймворков, использующих такой шаблон. Вместо этого, более распространенной моделью является использование первого подхода, описанного выше (минимальная стоимость для случая без исключения, но высокая стоимость, когда создаются исключения) для всех исключений.

Supercat
источник