Являются ли циклы do-while-false распространенными? - PullRequest
25 голосов
/ 11 сентября 2009

Некоторое время назад я переключил способ обработки ошибок стиля c.

Я обнаружил, что большая часть моего кода выглядит так:

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 ) обычно содержится больше кода, чем просто одного вызова функции. Впрочем, есть над чем подумать.

Ответы [ 20 ]

1 голос
/ 03 февраля 2013

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

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

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

{
    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 while, потому что мне нужен перерыв; способность также.

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

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

1 голос
/ 15 сентября 2009

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

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

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);

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

1 голос
/ 11 сентября 2009

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

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

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

if( ! ( error_val = doSomething()))

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

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

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

0 голосов
/ 07 апреля 2010

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

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


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;

в отличие от

<code>
pointerA * pa = NULL;
pointerB * pb = NULL;
pointerB * pc = NULL;</p>

<p>pa = new pointerA();
  if (!dosomethingWithPA( pa )) {
    delete pa;
    return FALSE;
  }</p>

<p>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;
  }</p>

<p>delete pa;
delete pb;
delete pc;
return TRUE;
0 голосов
/ 11 сентября 2009

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

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

0 голосов
/ 11 сентября 2009

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

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

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

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

0 голосов
/ 11 сентября 2009

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

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

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

0 голосов
/ 29 октября 2013

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

Пример общей парадигмы:

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

0 голосов
/ 14 июня 2016

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

Код становится

    ...
    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 и error, как в ответе на затмения.

    ...
    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;
    }
0 голосов
/ 11 сентября 2009

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

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

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