Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Прочее

Продолжение цикла и выход из него

В циклах иногда иногда бывает удобным проверить некое условие и при его выполнении выйти из цикла.
Goto must die
В Си-подобных языках для этой цели служит оператор «break». Однако бывают такие ситуации, когда условие выхода удобнее проверить не в заголовке цикла, а «по месту требования».

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

for (int i=0; i < Imax; ++i)
{
   // ...
   int  надо_выйти_из_цикла = FALSE;
   for (int j=0; i < Jmax; ++j)
   {
       // ...
       if (условие)
       {  надо_выйти_из_цикла = TRUE;
          break;
       }
       // ...
   }
   if (надо_выйти_из_цикла)
       break;
   // ...
}
        Не очень красивое решение. Вот поэтому подобные случаи являются оправданием «goto»:
for (int i=0; i < Imax; ++i)
{
   // ...
   for (int j=0; i < Jmax; ++j)
   {
       // ...
       if (условие)
          goto  end_loop;
       // ...
   }
   // ...
}
end_loop:;
        Это короче, элегантнее, но... с «goto»! Это тот самый «goto», который стал причиной многих психических заболеваний, банкротств и падений индекса NASDAQ. А нельзя ли так же кратко, но без любимого врага Дейкстры, о котором мы поговорим в следующей статье? Может, в других языках есть приёмы получше?

Java

В Java для избавления от «goto» применяется техника именованных блоков.
LOOP: 
for(int i=0; i < Imax; ++i)
{
   // ...
   for(int j=0; j < Jmax; ++j)
   {
      /// ...
      if (условие) 
          break LOOP;
      // ...
   }
   // ...
}
Хотя значительно нагляднее было бы так:
for(int i=0; i < Imax; ++i)
{
   // ...
   for(int j=0; j < Jmax; ++j)
   {
      /// ...
      if (условие) 
          break END_LOOP;
      // ...
   }
   // ...
} :END_LOOP; 
Есть сильное подозрение, что так не сделали только потому, что это очень смахивает на простую замену ключевого слова «goto» на «break».

PHP

         В PHP выход из вложенного цикла выглядит, на мой взгляд, значительно элегантнее. После «break» указывается количество вложенных циклов, которые должен «покинуть» оператор «break». В приведённом примере, который аналогичен приведённому выше для Java, «break» должен «пересечь» две фигурные скобки «}», чтобы оказаться за пределами двух циклов.
for($i=0; $i < $Imax; ++$i)
{
   // ...
   for($j=0; $j < $Jmax; ++$j)
   {
      // ...
      if(условие) 
          break 2;
      // ...
   }
   // ...
}
Разыскивается убийца GOTO


        Такое усовершенствование пришло в голову автору этих строк задолго до знакомства с PHP. Такое «параллельное изобретение» подверждает невозможность полной монополии на хорошие идеи. Но поскольку имелись идеи не только насчёт «break N», то при знакомстве PHP обратил внимание, что в нём нет «continue N». Идея, которая вполне «симметрична» предыдущей. Хотя для применения «continue N» не очень легко привести пример из жизни для иллюстрации его полезности. Возможно, читатели этих строк предложат что-то в комментариях к статье.

Итог



        И так, теперь нам необходимо найденные находки изложить в систематизированном виде в сочетании с нашим «симметричном скобочном» стиле:
(int i=0 loop i < $Imax; ++i
   // ...
   (int j=0 loop j < $Jmax; ++j
      // ...
      (if  условие
          exit 2)
      // ... 
   )
   // ...
}
        Замена ключевого слова «break» на «exit», как видится, больше соответствует смыслу выполняемого действия. Но это для тех, кто предпочитает программировать с использованием английских идентификаторов. Для приверженцев повсеместного употребления русской речи ключевое слово одно — «выход». Ну и теперь, конечно, было бы интересно узнать, какой графический (вместо синтаксического) сахар должна предлжить программисту IDE. Было бы логичным увидеть примерно такое:

  // Объемлющий цикл  
 
(       инициал.выр-я loop условие; выр-я в конце цикла  
  // Выход из объемлющего цикла  
 
(       if условие  
                           exit
 
  // Вложенный цикл  
 
(       инициал.выр-я loop условие; выр.в конце цикла  
  // Продолжение вложенного цикла
(       if условие  
                               continue )
 
     
  // Выход из вложенного цикла
(       if условие  
                                                 exit 2 )
 
  // Операторы цикла
 
  // В программе на С здесь бы поставили метку типа «end_loop:;»


        Получается очень наглядно. Стрелки от ключевого слова «exit» показывает, из какого цикла будет произведён выход. Если же «exit» имеет числовой операнд, то стрелка должна пересечь границы нескольких циклов. Величина операнда равна числу покидаемых циклов. Оператор «continue» сопровождается другого рода стрелкой, вид которой напоминает о возврате к началу цикла.

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

Последняя правка: 2014-12-20    14:51

ОценитеОценки посетителей
   ████████████████████████ 20 (55.5%)
   █████████ 7 (19.4%)
   ███ 2 (5.55%)
   █████████ 7 (19.4%)

Отзывы

     2013/05/07 16:37, koba

Вообще говоря, выход из глубоко вложенного цикла за пределы внешнего цикла является единственным оправданием любителей goto. В действительности такие любители не понимают, а их оппоненты не показывают (может быть, тоже не понимают), одного обстоятельства, которое показывает их слабое понимание принципов программирования. Посмотрим, в чём смысл нескольких вложенных циклов, из которых из самого внутреннего приходится делать выход? А смысл очень простой: проверяется некоторое множество абсолютно равноценных элементов, но это множество программист по тем или иным причинам разрезал на несколько измерений. То есть он некие элементы представил как неравноценные, но при этом хочет обрабатывать их как равноценные. Разумеется, это противоречие такие программисты не только не могут разрешить, но даже не знают о его существовании. То есть они сканирование множества в целом заменяют сканированием отдельных измерений. Поэтому им приходится использовать такой несуразный приём как выпрыгивание наружу.

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

Пример

Было с goto и вложенными циклами так:
int matrix[n][m];
int value;
...
for(int i=0; i<n; i++)
for (int j=0; j<m; j++)
if (matrix[i][j] == value) {
printf("value %d found in cell (%d,%d)\n",value,i,j);
goto end_loop;
}
printf("value %d not found\n",value);
end_loop: ;
Стало без goto и с одним циклом так (пример на C):
int matrix[n][m];
int value;
...
int i = 0;
int j = 0;
int f;

while (!((f = (matrix[i][j] == value)) !! (i == n) && (j == m)))
if (!(j = ++j % m)) i = ++i % n;

if (f) printf("value %d found in cell (%d,%d)\n",value,i,j);
else printf("value %d not found\n",value);

     2013/05/07 22:13, Автор сайта

Ну давайте представим, что у нас есть массив ссылок, который мы перебираем в объемлющем цикле. Элементы этого массива ссылаются, скажем, на ещё какие-то массивы неодинаковой длины. Эти массивы мы будем перебирать во внутреннем цикле. Не правда ли, это логично? И если перед нами стоит задача поиска, то найдя нужный элемент, мы смело даём команду «exit 2» и выходим обоих циклов. Заурядная задача, заурядное решение, но кратчайшим способом.

     2014/01/31 10:40, Pensulo

Пример решения предложенной Вами задачки на VisualBasic с применением всего одного цикла:
Const iMax As Integer = 100
Private Source(1 To iMax) As Variant
' Source - одномерный массив состоящий из ссылок
' на другие одномерные массивы переменной длинны.
Function SearchInArray(Required As String) As Boolean
Dim i As Integer: Dim j As Integer
  i = 1: SearchInArray = False
  Do
    j = LBound(Source(i)) ' Получить начальный индекс переменного массива
    If Source(i)(j) = Required Then
      SearchInArray = True
      Exit Do  ' В данном примере можно сразу употребить 'Exit Function'
    End If
    j = j + 1
    If j > UBound(Source(i)) Then
  ' Анализировать выход за конечный индекс переменного массива
      i = i + 1
    End If
  Loop Until i > iMax
End Function
Аналогично эту задачу можно решить на любом ЯВУ без употребления оператора выхода из объемлющего (внешнего) цикла.

Решение с двумя циклами привожу следом. Отмечу при этом, что и в этом случае не потребовался оператор выхода из объемлющего (внешнего) цикла:
Function SearchInArray2(Required As String) As Boolean
Dim i As Integer: Dim j As Integer
  i = 1: SearchInArray2 = False
  Do
    j = LBound(Source(i))
    Do
      If Source(i)(j) = Required Then
        SearchInArray2 = True
        Exit Do 'Опять же в данном примере можно сразу употребить 'Exit Function'
      End If
      j = j + 1
    Loop Until j > UBound(Source(i))
    If SearchInArray2 = True Then
    ' Этот анализ можно опустить в случае использования 'Exit Function' ранее
      Exit Do
    End If
    i = i + 1
  Loop Until i > iMax
End Function

     2014/01/31 16:13, Автор сайта

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

     2014/02/25 15:49, Руслан

Более короткое далеко не всегда лучше чем более длинное решение. В любом случае решение по феншую будет лучше.

     2014/07/02 05:56, utkin

Получается очень наглядно. Стрелки от ключевого слова «exit» показывает, из какого цикла будет произведён выход. Если же «exit» имеет числовой операнд, то стрелка должна пересечь границы нескольких циклов. Величина операнда равна числу покидаемых циклов. Оператор «continue» сопровождается другого рода стрелкой, вид которой напоминает о возврате к началу цикла.

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

     2014/07/02 13:37, Автор сайта

В IDE должны быть настройки, которые включают или отключают показ тех или иных графических элементов, например: «сопровождать стрелкой оператор выхода», «сопровождать условные операторы значками» и т.д. Тогда каждый настроит себе по вкусу.

     2014/11/07 11:30, Сергей

2 koba Сокращение уровня вложенности циклов («уплощение») является чисто техническим приёмом, который в данной ситуации имеет только одну цель - избежать выхода из глубоко вложенного цикла. Однако результат достигается за счёт потери наглядности и понятности кода. Использование «exit 2» в данной ситуации не несёт никаких вредных последствий, полностью сохраняет эффективность кода и, самое важное, не причиняет вреда лёгкости чтения и понимания кода.

     2014/11/27 09:06, enburk

+1
Даже вариант с goto читается мгновенно. В исправленный же вариант приходится вглядываться и размышлять, что замыслил автор...

Предложил бы еще вместо «exit 2» писать «exit exit».

     2014/11/27 17:41, Автор сайта

Тоже вариант. Вот только при обходе многомерного массива в цикле нужно будет писать «exit exit ... exit».

     2015/11/02 01:04, Nick

2 Сергей
+1

2 coba
но если уж на то пошло, то для С можно было и попроще написать:
int matrix[n][m];
int value;
int f;

for(int i = n * m; --i >= 0 && f = (matrix[i] != value););

if (!f) printf("value %d found in cell (%d,%d)\n",value,i,j);
else printf("value %d not found\n",value);
И вообще все эти continue, break и т.д. просто синтаксический сахар.
continue легко заменяется оператором if() {}. break не совсем конечно сахар, т.к. для замены дополнительная переменная нужна, но если бы goto не злоупотребляли, то и break не понадобился бы... (но "бы" мешает)
P.S. Даже эта запись
int matrix[n][m];
– синтаксический сахар. Но если из языков убрать этот сахар, будет ассемблер.

     2015/11/08 19:28, Автор сайта

Двухмерные матрицы – самый простой способ показать случай, когда «goto» полезен, потому что короче. При отсутствии иных вариантов, конечно. Но «break N» элегантно устраняет этот наверное единственный обоснованный случай применения «goto» – не правда ли? А для работы с элементами множеств лучше использовать цикл «foreach».

     2016/07/03 13:36, rst256

Нет не правда, там не нужен goto? Как и не нужен лишний цикл, хороший правильный вариант тут уже продемонстрировали.
А вот первый попавший под руку код с goto:
static size_t utf8_decode(const char *s, const char *e, unsigned *pch) {
unsigned ch;

if (s >= e) {
*pch = 0;
return 0;
}

ch = (unsigned char)s[0];
if (ch < 0xC0) goto fallback;
if (ch < 0xE0) {
if (s+1 >= e !! (s[1] & 0xC0) != 0x80)
goto fallback;
*pch = ((ch & 0x1F) << 6) !
(s[1] & 0x3F);
return 2;
}
if (ch < 0xF0) {
if (s+2 >= e !! (s[1] & 0xC0) != 0x80
!! (s[2] & 0xC0) != 0x80)
goto fallback;
*pch = ((ch & 0x0F) << 12) !
((s[1] & 0x3F) << 6) !
(s[2] & 0x3F);
return 3;
}
{
int count = 0; /* to count number of continuation bytes */
unsigned res = 0;
while ((ch & 0x40) != 0) { /* still have continuation bytes? */
int cc = (unsigned char)s[++count];
if ((cc & 0xC0) != 0x80) /* not a continuation byte? */
goto fallback; /* invalid byte sequence, fallback */
res = (res << 6) ! (cc & 0x3F); /* add lower 6 bits from cont. byte */
ch <<= 1; /* to test next bit */
}
if (count > 5)
goto fallback; /* invalid byte sequence */
res != ((ch & 0x7F) << (count * 5)); /* add first byte */
*pch = res;
return count+1;
}

fallback:
*pch = ch;
return 1;
}
Если код под fallback кажется вам достаточно небольшим, чтобы расплодить его заместо вызова goto, то представьте в добавок нему еще пару сотен строк, сути это не поменяет

     2016/07/03 15:36, Автор сайта

static size_t utf8_decode(const char *s, const char *e, unsigned *pch) {
unsigned ch;

if (s >= e) {
*pch = 0;
return 0;
}

ch = (unsigned char)s[0];
if (ch < 0xC0) fallback(*pch, ch); return 1;
if (ch < 0xE0) {
if (s+1 >= e !! (s[1] & 0xC0) != 0x80)
fallback(*pch, ch); return 1;
*pch = ((ch & 0x1F) << 6) !
(s[1] & 0x3F);
return 2;
}
if (ch < 0xF0) {
if (s+2 >= e !! (s[1] & 0xC0) != 0x80
!! (s[2] & 0xC0) != 0x80)
fallback(*pch, ch); return 1;
*pch = ((ch & 0x0F) << 12) !
((s[1] & 0x3F) << 6) !
(s[2] & 0x3F);
return 3;
}
{
int count = 0; /* to count number of continuation bytes */
unsigned res = 0;
while ((ch & 0x40) != 0) { /* still have continuation bytes? */
int cc = (unsigned char)s[++count];
if ((cc & 0xC0) != 0x80) /* not a continuation byte? */
fallback(*pch, ch); return 1;/* invalid byte sequence, fallback */
res = (res << 6) ! (cc & 0x3F); /* add lower 6 bits from cont. byte */
ch <<= 1; /* to test next bit */
}
if (count > 5)
fallback(*pch, ch); return 1; /* invalid byte sequence */
res != ((ch & 0x7F) << (count * 5)); /* add first byte */
*pch = res;
return count+1;
}
}
Так сойдёт? Но если бы из функции можно было возвращить несколько значений, то «return 1, pch» смотрелось бы красивее.

     2016/08/10 18:55, rst256

Так сойдёт? Но если бы из функции можно было возвращить несколько значений, то «return 1, pch» смотрелось бы красивее.

Наверное да. Я по, крайней мере, поступил именно так при адаптации данного кода под лексер, у вас ведь тоже, как я понимаю, fallback(*pch, ch) — это макрос?

     2017/09/19 13:27, Comdiv

Многие видели заголовок статьи Дейкстры, но немногие её читали. Дейкстра возражает против непоследовательных процессов, а goto - всего лишь способ нарушения, который присутствовал в языках того времени. Теперь наплодили много специализированных аналогов goto для частных случаев, посчитав, что они решили проблему, но несмотря на то, что такие ограниченные goto лучше, чем полноценный goto, это не решает проблему принципиально. Там, где критически важна корректность кода, неструктурные выходы не практикуются. Примером может служить MISRA C, являющаяся стандартом де факто для встраиваемых систем в автомобилях. В этих требованиях неструктурные переходы запрещены почти полностью, включая преждевременный return. Исключение сделано для единственного break на цикл.

     2017/09/18 22:36, Автор сайта

Я задавал вопрос Андрею Карпову, разработчику статического анализатора кода PVS-Studio, как влияет на качество работы анализатора оператор «goto», а так же «break», «continue» и «return». Ответил одним словом: «Плохо». Правда, непонятно, отнёс он это только к «goto», или к другим операторам тоже. Обещал написать статью на эту тему, но что-то не идёт к нему вдохновение...

Написать отзыв

Написать автору можно на электронную почту mail(аt)compiler.su

Авторизация

Регистрация

Выслать пароль

Карта сайта


Каким должен быть язык программирования?

Анализ и критика

Устарел ли текст как форма представления программы

Русский язык и программирование

Многоязыковое программирование

Синтаксис языков программирования

Синтаксический сахар

Некоторые «вкусности» Алгол-68

«Двухмерный» синтаксис Python

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

Должна ли программа быть удобочитаемой?

Стиль языка программирования

Тексто-графическое представление программы

●  Разделители

●  Строки программы

●  Слева направо или справа налево?

Комментарии

●  Длинные комментарии

●  Короткие комментарии

●  Комментарии автоматической генерации документации

●  Нерабочий код

Нужны ли беззнаковые целые?

Шестнадцатиричные и двоичные константы

Условные операторы

Переключатель

Циклы

●  Продолжение цикла и выход из него

Некошерный «goto»

Операции присвоения и проверки на равенство. Возможно ли однаковое обозначение?

Так ли нужны операции «&&», «||» и «^^»?

Постфиксные инкремент и декремент

Почему в PHP для конкатенации строк используется «.»?

Указатели и ссылки в C++

Использование памяти

Почему динамическое распределение памяти – это плохо

Как обеспечить возврат функциями объектов переменной длины?

●  Типы переменного размера (dynamically sized types, DST) в языке Rust

●  Массивы переменной длины в C/C++

●  Размещение объектов в стеке, традиционный подход

●  Размещение объектов переменной длины с использованием множества стеков

●  Размещение объектов переменной длины с использованием двух стеков

●  Реализация двухстековой модели размещения данных

●  Двухстековая модель: тесты на скорость

●  Размещение объектов переменной длины с использованием одного стека

Можно ли забыть о «куче», если объекты переменной длины хранить в стеке

Безопасность и размещение объектов переменной длины в стеке

Массивы, структуры, типы, классы переменной длины

О хранении данных в стеке, вместо заключения

Описание языка

Компилятор

Отечественные разработки

Cтатьи на компьютерные темы

Компьютерный юмор

Прочее

Последние комментарии

2018/06/14 00:37, rst256
Лень — двигатель прогресса

2018/05/31 18:52, rst256
Программирование без программистов — это медицина без врачей

2018/05/31 17:57, rst256
Циклы

2018/05/31 17:50, Comdiv
Разбор цепочек знаков операций

2018/05/31 17:42, Comdiv
Как отличить унарный минус от бинарного

2018/05/30 18:57, Александр Коновалов aka Маздайщик
Раскрутка компилятора

2018/05/29 21:52, Автор сайта
Указатели и ссылки в C++

2018/05/28 20:29, Александр Коновалов aka Маздайщик
Анонс будущих статей