Исключения против кодов возврата: мы что-то теряем (получая что-то другое)?

Мой вопрос довольно расплывчатый: о) - Но вот пример:

Когда я писал код на C, я мог регистрировать значение счетчика, когда что-то не получалось:

   <...>
   for ( int i = 0 ; i < n ; i++ )
      if ( SUCCESS != myCall())
         Log( "Failure, i = %d", i );
   <...>

Теперь, используя исключения, я получаю это:

  try
   {
      <...>
      for ( int i = 0 ; i < n ; i++ )
         myCall();
      <...>
   }
   catch ( Exception exception )
   {
      Log( "Failure ! Maybe in myCall() ? Don't know. i's value ? No clue." );
   }

Конечно, можно объявить "i" вне оператора try/catch (и это то, что я делаю). Но мне это не нравится - мне нравится объявлять переменные там, где они используются, а не раньше.

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

Заранее спасибо! Sylvain.

ДОБАВЛЕНО: myCall() - непонятный вызов API - я понятия не имею, что он может выдать. Кроме того, я могу, конечно, добавить блок Try/Catch вокруг каждого вызова, но тогда мне лучше использовать коды возврата? Могу ли я добавить много шума вокруг важных строк кода?

15 ответов

Решение

Как насчет:

for(int i = 0; i < n; i++)
{
  try
  {
    myCall();
  }
  catch(Exception e)
  {
    Log(String.Format("Problem with {0}", i));
  }
}

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

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

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

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

Теперь, в реальном мире, часто случается, что методы генерируют исключения там, где вы предпочли бы, чтобы они этого не делали (например, EndOfFile), и в этом случае вы должны использовать анти-шаблон "try / catch wrapper", но если вы разрабатываете свои методы, не используйте исключения для повседневной обработки ошибок - используйте их только для исключительных обстоятельств. (да, я знаю, что правильно подобрать такой дизайн сложно, но так много в дизайне)

Мне не нравится выражение "сейчас, за исключением...".

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

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

Если вы думаете, что ваша неординарная альтернатива лучше - придерживайтесь ее!

Да уж. 2 вещи.

Поместите блоки try-catch там, где они имеют смысл. Если вас интересуют исключения из myCall (а также значение i), используйте

for ( int i = 0 ; i < n ; i++ )
    try { myCall(); } catch ( Exception exception ) {
        Log( "Failure, i = %d", i );
    }

Бросайте объекты разных классов за разные ошибки. Если вас интересуют логические ошибки, возникающие при финансовой обработке, бросьте finances::logic_error, а не std::exception("error msg") или что-то в этом роде. Таким образом, вы можете поймать то, что вам нужно.

Примите во внимание мнение Раймонда Чена, а также мышление Microsoft о x64.
((Raymond Chen Исключения)), поскольку Google-запроса достаточно, чтобы привести вас к его классическим эссе "Более чистый, более элегантный и неправильный - только то, что вы не видите пути ошибки, не означает, что его не существует". ". и разъяснение "Чище, элегантнее и труднее распознать".
((модель исключений x64)) приводит вас к статье MSDN "Все, что вам нужно знать для программирования 64-битных систем Windows", в которой содержится цитата "Оборотная сторона обработки исключений на основе таблиц (относительно стека x86) модель) заключается в том, что поиск записей в таблице функций по адресам кода занимает больше времени, чем просто просмотр связанного списка. Преимущество заключается в том, что функциям не приходится тратить время на установку блока данных try при каждом ее выполнении ".
Подводя итог этой цитате, в x64 можно бесплатно или почти бесплатно установить "catch", который никогда не используется, но на самом деле throw-catch исключение медленнее, чем в x86.

Две вещи здесь, с моей точки зрения.

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


   public void doStuff(int arg) {
      if (arg < 0) {
         throw new InvalidArgumentException("Index must be greater than or equal to zero, but was " + arg);
      }
      ...

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

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

try
   {
      <...>
      for ( int i = 0 ; i < n ; i++ )
         DebugLog("Trying myCall with i = " + i);
         myCall();
      <...>
   }
   catch ( Exception exception )
   {
      Log( "Failure ! Maybe in myCall() ? Don't know. i's value ? No clue." );
   }
Таким образом, если что-то пойдет не так, вы можете просмотреть свой журнал отладки с высокой степенью детализации и выяснить, что i было незадолго до вызова, который бросил исключение.

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

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

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

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

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

public class SomeException : Exception
{
     public int Iteration;

     public SomeException(int iteration) { Iteration = iteration; }
}

Блок цикла:

try
{
    for(int i = 0; i < n; ++i)
    {
        myCall(i);
    }
}catch(SomeException se)
{
    Log("Something terrible happened during the %ith iteration.", se.Iteration);
}

И, наконец, функция myCall ():

void myCall(int iteration)
{
    // do something very useful here

    // failed.
    throw new SomeException(iteration);
}

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

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

Конечно, можно объявить "i" вне оператора try/catch (и это то, что я делаю).

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

Перво-наперво. Если вы ловите исключение, вы ошибаетесь. Вы должны ловить конкретное исключение, которое вы ожидаете.

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

Например, ArrayIndexOutOfBoundsException будет содержать адресуемый индекс.

Что ж! Вы могли бы сделать это:

   try
   {
      <...>
      for ( int i = 0 ; i < n ; i++ )
         try {
            myCall();
         } catch(Exception e) {
            println("bloody hell! " + i);
         }
      <...>
   }

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

Многие инструменты обработки исключений ( MadExcept for Delphi - один) позволяют вам извлекать всю трассировку стека при обнаружении исключения. Итак, вы бы точно знали , куда его бросили.

Обычный метод получения "i" - перехват исключения и добавление к нему дополнительных данных (метод istream) перед его сбросом. Это редко, что нужно, но если вы настаиваете...

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

Другие вопросы по тегам