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

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

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

Но, увидев мою порочную природу, я добавлю немного оскорблений к этой травме, расширив это поведение, скажем, до типа Overridden 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. Уважайте контракт вашего метода

Пристрели меня.

Ответы [ 24 ]

160 голосов
/ 08 апреля 2009

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

У меня есть.

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

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

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

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

150 голосов
/ 08 апреля 2009

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

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

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

25 голосов
/ 08 апреля 2009

Мое эмпирическое правило:

  • Если вы можете сделать что-нибудь для восстановления после ошибки, перехватывайте исключения
  • Если ошибка очень распространенная (например, пользователь попытался войти в систему с неправильным паролем), используйте 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.. :(

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

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

22 голосов
/ 08 апреля 2009

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

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

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

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

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

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

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

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

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

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

15 голосов
/ 13 июля 2013

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

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

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

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

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

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

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

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

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

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

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

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

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

11 голосов
/ 08 апреля 2009

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

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

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

8 голосов
/ 08 апреля 2009

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

8 голосов
/ 08 апреля 2009

Джош Блох активно занимается этой темой в Effective 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 вызывающего.

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

7 голосов
/ 08 апреля 2009

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

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

public Throwable fillInStackTrace() { return this; }

В таком исключении не будут заполнены трассировки стека.

5 голосов
/ 08 апреля 2009

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

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

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