Обстояны ли ложные петли?

А назад я переключил способ обработки ошибок стиля.

Я нашел, что мой код выглядел так:

int errorCode = 0;

errorCode = doSomething();
if (errorCode == 0)
{
   errorCode = doSomethingElse();
}

...

if (errorCode == 0)
{
   errorCode = doSomethingElseNew();
}

Но в последнее время я писал это так:

int errorCode = 0;

do
{       
   if (doSomething() != 0) break;
   if (doSomethingElse() != 0) break;
   ...
   if (doSomethingElseNew() != 0) break;
 } while(false);

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

Изменить: только для пояснения, обычно эта конструкция использует errno, иначе я буду присваивать значение int перед взломом. Также обычно больше кода, чем только один вызов функции в предложениях if (error == 0 ). Много хороших моментов, чтобы думать, хотя.

Ответ 1

Второй фрагмент выглядит неправильно. Вы действительно повторно изобрели goto.

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

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

Ответ 2

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

    int errorCode = 0;

    if ((errorCode = doSomething()) != 0) goto errorHandler;
    if ((errorCode = doSomethingElse()) != 0) goto errorHandler;
      ...
    if ((errorCode = doSomethingElseNew()) != 0) goto errorHandler;

    return;
errorHandler:
    // handle error

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

    int errorCode = 0;

    if ((errorCode = doSomething()) != 0) goto errorHandler;
    if ((errorCode = doSomethingElse()) != 0) goto errorHandler;
      ...
    if ((errorCode = doSomethingElseNew()) != 0) goto errorHandlerSomethingElseNew;

    return;
errorHandler:
    // handle error
    return;
errorHandlerSomethingElseNew:
    // handle error
    return;

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

    int errorCode = 0;

    if ((errorCode = doSomething()) != 0) goto errorHandler;
    if ((errorCode = doSomethingElse()) != 0) goto errorHandler1;
      ...
    if ((errorCode = doSomethingElseNew()) != 0) goto errorHandler2;

errorHandler2:
    // clean up after doSomethingElseNew
errorHandler1:
    // clean up after doSomethingElse
errorHandler:
    // clean up after doSomething
    return errorCode;

Эта идиома дает вам преимущество в том, что вы не повторяете свой код очистки (конечно, если вы используете С++, RAII еще лучше очистит код очистки.

Ответ 3

Первый стиль - это образец, который опытный глаз бросает сразу.

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

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

Ответ 4

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

Конечно, исключения превосходят оба...

Ответ 5

Сделайте его коротким, компактным и легко читаемым?

Как насчет:

if ((errorcode = doSomething()) == 0
&&  (errorcode = doSomethingElse()) == 0
&&  (errorcode = doSomethingElseNew()) == 0)
    maybe_something_here;
return errorcode; // or whatever is next

Ответ 6

Почему бы не заменить do/while и не сломаться с помощью функции и вместо этого вернется?

Вы заново изобрели goto.

Ответ 7

Как насчет использования исключений?

try {
  DoSomeThing();
  DoSomethingElse();
  DoSomethingNew();
  .
  .
  .
}
catch(DoSomethingException e) {
  .
  .
}
catch(DoSomethingElseException e) {
  .
  .
}
catch(DoSomethingNewException e) {
  .
  .
}
catch(...) {
  .
  .
}

Ответ 8

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

Первый может получить ДЕЙСТВИТЕЛЬНО раздражающий после того, как ваш код достигнет определенного размера, потому что у него много шаблонов.

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

fn() {
    while(true) {
        if(doIt())
            handleError();//error detected...
    }
}

bool doIt() {
    if(!doThing1Succeeds())
        return true;
    if(!doThing2Succeeds())
        return true;
    return false;
}

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

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

Ответ 9

Это должно быть сделано через исключения, по крайней мере, если тег С++ верен. Нет ничего плохого в том, что вы используете только C, хотя я предлагаю использовать Boolean вместо этого, поскольку вы не используете возвращенный код ошибки. Вам не нужно набирать!= 0 либо тогда...

Ответ 10

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

Соответствующие экспонаты: файлы sqlstmt.ec, upload.ec, reload.ec из SQLCMD исходный код (не, а не самозванец Microsoft, мой). Расширение '.ec' означает, что файл содержит ESQL/C - Embedded SQL в C, который предварительно обработан до простого C; вам не нужно знать ESQL/C, чтобы увидеть структуры циклов. Все петли обозначаются:

    /* This is a one-cycle loop that simplifies error handling */

Ответ 11

Классическая C-идиома:

if( (error_val = doSomething()) == 0)
{ 
   //Manage error condition
}

Обратите внимание, что C возвращает назначенное значение из присваивания, позволяя выполнить тест. Часто люди пишут:

if( ! ( error_val = doSomething()))

но я сохранил == 0 для ясности.

Относительно ваших идиом...

Ваша первая идиома в порядке. Ваша вторая идиома - злоупотребление языком, и вы должны избегать этого.

Ответ 12

Как насчет этой версии, тогда

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

bool statusOkay = true;

if (statusOkay)
    statusOkay = (doSomething() == 0);

if (statusOkay)
    statusOkay = (doSomethingElse() == 0);

if (statusOkay)
    statusOkay = (doSomethingElseNew() == 0);

Но если вы действительно увлечены своей второй техникой, вы можете рассмотреть этот подход:

bool statusOkay = true;

statusOkay = statusOkay && (doSomething() == 0);
statusOkay = statusOkay && (doSomethingElse() == 0);
statusOkay = statusOkay && (doSomethingElseNew() == 0);

Просто не ожидайте, что программисты по обслуживанию поблагодарят вас!

Ответ 13

Я использую do { } while (false); каждый раз, когда это кажется подходящим. Я вижу, что это что-то вроде блока try/catch, поскольку у меня есть код, который настроен как блок с серией решений с возможными исключениями, и необходимо, чтобы различные пути через правила и логику сливались в конце блока.

Я уверен, что использую эту конструкцию только с программированием на C, и это не очень часто.

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

{
    int iCallStatus = 0;
    iCallStatus = doFunc1();
    if (iCallStatus == 0) iCallStatus = doFunc2();
    if (iCallStatus == 0) icallStatus = doFunc3();
}

Это коротко, и смысл прост и понятен даже без комментариев.

Время от времени я сталкивался с тем, что этот довольно простой последовательный поток процедурных шагов не применяется к конкретному требованию. Мне нужно создать блок кода с различными решениями, обычно включающими циклы или итерации по некоторым сериям объектов данных, и я хочу рассматривать эту серию как своего рода транзакцию, в которой транзакция будет совершена, если нет ошибки или прервана если есть какое-то условие ошибки, обнаруженное во время обработки транзакции. Как часть этого блока данных, я могу создать набор временных переменных для области do { } while (false); Когда я когда-либо использую это, я всегда помещаю комментарий, указывающий, что это единственная итерация, пока что-то вроде:

do {  // single loop code block begins
    // block of statements for business logic with single ending point
} while (false);  // single loop code block ends

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

Причина, по которой я предпочитаю эту конструкцию с использованием инструкции goto, заключается в том, что использование скобок и отступов облегчает чтение исходного кода. С моим редактором я могу легко найти верхнюю и нижнюю части блока, а отступы упрощают визуализацию кода как блока с одной точкой входа и известной конечной точкой. Внутри блока может быть несколько точек выхода, но я знаю, где они все закончится. Использование этого означает, что я могу создать локализованные переменные, которые выйдут из области видимости, но просто используя скобки без do { } while (false);. Однако я использую do, потому что мне нужна возможность break;.

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

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

Ответ 14

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

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

Ответ 15

Для меня я бы предпочел:

if(!doSomething()) {
    doSomethingElse();
}
doSomethingNew();

Все остальное - синтаксический шум, который скрывает три вызова функций. Внутри Else и New вы можете выбросить ошибку или, если старше, использовать longjmp, чтобы вернуться к предыдущей обработке. Хороший, чистый и довольно очевидный.

Ответ 16

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

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

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

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

Ответ 17

Честно говоря, более эффективные программисты на C/С++, которые я знал, просто использовали бы gotos в таких условиях. Общий подход заключается в том, чтобы иметь один ярлык выхода со всей очисткой после него. Имейте только один путь возврата от функции. Когда логика очистки начинает усложняться/иметь условные обозначения, то разбить функцию на подфункции. Это довольно типично для системного кодирования в C/С++ imo, где API-интерфейсы, которые вы вызываете, возвращают коды ошибок, а не бросают исключения.

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

Ответ 18

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

Примером общей парадигмы является следующее:

int status = 0;

// declare and initialize resources
BYTE *memory = NULL;
HANDLE file = INVALID_HANDLE_VALUE;
// etc...

do
{
  // do some work

  // allocate some resources
  file = CreateFile(...);
  if(file == INVALID_HANDLE_VALUE)
  {
    status = GetLastError();
    break;
  }

  // do some work with new resources

  // allocate more resources

  memory = malloc(...);
  if(memory == NULL)
  {
    status = ERROR_OUTOFMEMORY;
    break;
  }

  // do more work with new resources
} while(0);

// clean up the declared resources
if(file != INVALID_HANDLE_VALUE)
  CloseHandle(file);

if(memory != NULL)
  free(memory);

return status;

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

Ответ 19

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

Затем код становится

    ...
    int errorCode = doItAll();
    ...


    int doItAll(void) {
      int errorCode;
      if(errorCode=doSomething()!=0)
        return errorCode;
      if(errorCode=doSomethingElse()!=0)
        return errorCode;
      if(errorCode=doSomethingElseNew()!=0)
        return errorCode;
      return 0;
    }

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

    ...
    int errorCode = doItAll();
    ...


    int doItAll(void) {
      int errorCode;
      void * aResource = NULL; // Somthing that needs cleanup after doSomethingElse has been called
      if(errorCode=doSomething()!=0) //Doesn't need cleanup
        return errorCode;
      if(errorCode=doSomethingElse(&aResource)!=0)
        goto cleanup;
      if(errorCode=doSomethingElseNew()!=0)
        goto cleanup;
      return 0;
    cleanup:
      releaseResource(aResource);
      return errorCode;
    }

Ответ 20

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

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


pointerA * pa = NULL;
pointerB * pb = NULL;
pointerB * pc = NULL;
BOOL bRet = FALSE;
pa = new pointerA();
do {
  if (!dosomethingWithPA( pa ))
    break;
   pb = new poninterB();
  if(!dosomethingWithPB( pb ))
    break;
  pc = new pointerC();
  if(!dosemethingWithPC( pc ))
    break;
  bRet = TRUE;
} while (FALSE);

//
// cleanup
//
if (NULL != pa)
 delete pa;
if (NULL != pb)
 delete pb;
if (NULL != pc)
 delete pc;

return bRet;

в отличие от


pointerA * pa = NULL;
pointerB * pb = NULL;
pointerB * pc = NULL;

pa = new pointerA(); if (!dosomethingWithPA( pa )) { delete pa; return FALSE; }

pb = new poninterB(); if(!dosomethingWithPB( pb )) { delete pa; delete pb; return FALSE; } pc = new pointerC(); if(!dosemethingWithPAPBPC( pa,pb,pc )) { delete pa; delete pb; delete pc; return FALSE; }

delete pa; delete pb; delete pc; return TRUE;