Главная страница Visual 2000 · Общий список статей

"Go to" — выражение из четырех букв

Андрей Колесов

© Андрей Колесов, 2001
Авторский вариант. Статья была опубликована cо значительными сокращениями в журнале BYTE/Россия N 8/2001.
На эту тему см. также Размышления бывшего программиста

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


Если приглядеться внимательно...

Если приглядеться внимательно, то легко заметить, что, несмотря на очевидный прогресс инструментов программирования, методология разработки базируется на принципах, сформулированных еще в 60-70-х годах прошлого столетия. Из всех публикаций того времени мне хотелось бы выделить две фундаментальные книги (были, конечно, и другие), на которые я буду ссылаться далее:

  1. Э.Дейкстра "Заметки по структурному программированию" (в составе сборника "Структурное программирование" / М.: "Мир", 1975.

  2. Э.Йордан "Структурное проектирование и конструирование программ" / М.: "Мир", 1979.

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

Казалось бы, статья Андрей Калинина "Разумный GoTo" (Byte/Россия N 4/2001), как раз демонстрирует преемственность программистских проблем, высказывая "еще одно мнение в споре об операторе GoTo". Тут очень бы хотелось вспомнить о диалектическом развитии по спирали, когда история повторяется на более высоком уровне. Но, к сожалению, аналогия со спиралью тут годится с обратным знаком — уровень обсуждения темы существенно слабее, а полученные автором выводы выглядят весьма сомнительно.

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

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

В начало статьи

Переходите дорогу только в установленных местах

Собственно отношение к оператору безусловного перехода сформулировано достаточно давно и очень четко:

  1. Использование GoTo в программе автоматически создает серьезную потенциальную угрозу устойчивости и надежности программ.

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

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

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

Тут вполне уместна такая аналогия: действительно, можно сэкономить время, перебегая Тверскую улицу в Москве минуя пешеходный переход. Но многие ли из тех, кто делает так, сможет посоветовать своим детям "делай как я"?

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

В начало статьи

Методы познания: наука и религия (раздел не попал в печатный вариант)

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

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

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

В начало статьи

Программирование — наука или искусство? Технология!

Однако, ключевая идея г-на Калинина, с которой я хотел бы выразить свое несогласие, сформулирована в конце его статьи: "Я видел исходные тексты, в которых безусловные переходы используются далеко не лучшим способом. Но в этом виновата не языковая конструкция, а программист, который ее так использовал". В этой связи на память приходит цитата из "Швейка": "А если где кого убили, то так тому и надо — не будь дураком и не давай себя убивать".

70-е годы мне представляются переломным моментом в истории программирования, потому что именно тогда в дискуссии на тему "Что такое программирование? Это — наука или искусство?" был найден правильный ответ — "технология". А главная идея любой технологии — создание таких условий, когда производственный процесс минимально зависит от субъективных факторов. В частности, это означает, что современные средства программирования должны обеспечивать максимальную защиту от возможных ошибок разработчика.

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

В начало статьи

Эффективность программ. А что это такое?

Г-н Калинин объясняет возможность использования "запрещенных" методов необходимостью повышения эффективности программ. Однако, что характерно, он ничего не говорит о том, что понимается под "эффективностью".

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


For i = 1 To 10
  a(i) = 0
Next i

на


a(1) = 0
...
a(10) = 0

Не нужны тогда и процедуры — программа будет работать быстрее, если вместо каждого вызова процедуры будет на этапе трансляции вставлять копии полного кода. И т.д.

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

Конечно же, я готов согласиться с г-ном Калининым, что для аварийного выхода из последовательности вложенных циклов, наверное (почему "наверное" мы разберемся позднее), удобнее использовать безусловный переход. Хотя столь же легко можно обойтись и без него, выделив эту конструкцию в виде процедуры:


Sub MyProc
  For i = 1 To 10
    ...  ' тут может быть много вложенных циклов
    Exit Sub  ' аварийный выход
  Next i
End Sub

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

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

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

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

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

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

В начало статьи

Структурное программирование — основа индустриальной технологии

Необходимость разработки методов структурного программирования на рубеже 60-70-х годов была вызвана потребностями компьютерной индустрии. (Отмечу, что объектно-ориентированное программирование не противоречит идеям структурного, так как фактически является развитием последнего.) Объем программных проектов быстро возрастал. В этой связи хотелось бы напомнить, что один из крупнейших программных проектов до сегодняшних дней остается создание операционной системы IBM OS/360, первый вариант которой разрабатывался в 63-66 гг. В нем участвовали сотни программистов, а общая трудоемкость оценивалась в 5 тыс. человеко-лет.

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

Примечание. О Дейкстре, Йодане, Кнуте и многих других отцов-основателей современных технологий программирования существует много историй, которые составляют фольклор вычислительной науки. Например, Дейкстре принадлежит высказывание, что если знание Фортрана можно сравнить с младенческим расстройством, то ПЛ-1 — это определенно фатальная болезнь. (Конечно, речь идет о стандарте Фортран 66.) А вот две фразы из книги Йодана: "Программисту можно простить многие прегрешения, но ни одному из них, как бы он не был умен и опытен, нельзя простить программу, не оформленную и не содержащую комментариев".

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

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

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

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

В те времена огромный объем программирования был связан с использованием ассемблеров, т.е. машинноориентированных языков, единственные средства управления логикой, фактически ограничивались двумя операторами — условного (If <условие> GoTo) и безусловного (GoTo) перехода. Синтаксис языков высокого уровня также был недостаточно хорош, чтобы отказаться от GoTo.

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

Вообще говоря, главное в структурном программировании — грамотное составление правильной логической схемы программы, реализация которой языковыми средствами — дело вторичное. И уж во всяком случае, структуризация кода не сводится лишь к исключению GoTo. Доказывая эти положения, Йодан привел программные примеры реализации нужного набора логических конструкций (с использованием GoTo!) на пяти ведущих тогда языках: Assembler IBM 360, Fortran, Cobol, Algol и PL-1.

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

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

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

Книга Йодана появилась лишь спустя полтора года после старта нашего проекта. Огромное впечатление от нее во многом объяснялось тем, что в ней мы нашли систематическое изложение идей и подходов, к которым пришли сами путем проб и ошибок. Разумеется, в ней мы почерпнули также много нового и полезного.

В начало статьи

В чем суть структурного программирования

Идея тут достаточно проста: программа должна представлять собой множество вложенных блоков (или подругому — блоков, объединенных в виде иерархической древовидной структуры), каждый из которых имеет один вход и один выход. Чтобы нагляднее представить это, откройте, например, дерево каталогов в Проводнике Windows (рис.1) и представьте себе, что значок каталога соответствует такому блоку, а иконка файла — вычисляемому оператору. При этом передача управления между блоками и операторами на каждом уровне дерева выполняется последовательно. (По большому счету событийная модель программ организована точно также, просто роль связующего блока верхнего уровня выполняет ядро системы.)

Рис. 1

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

Ответ на это был дан в классической работе Бома и Джакопини, опубликованной в 1965 году, в которой было доказано (без всяких "нам кажется"!), что любая программа может быть построена с использованием лишь трех основных типов блоков:

  1. функциональный блок — отдельный линейный оператор или их последовательность;

  2. обобщенный цикл — конструкция типа Do <условие>...Loop (проверка в начале цикла!);

  3. принятие двоичного решения — If <условие> Then...Else.

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

Однако на практике оказалось, что хотя перечисленных выше управляющих блоков достаточно для построения программы, но для более эффективной работы желательно иметь некоторые "расширенные" варианты. Таким образом, появились конструкции Do... Loop <условие>, For...Next, и Select Case. Кроме того, для циклов и функциональных блоков желательно иметь операторы досрочного выхода из блока — Exit Do, Exit For и Exit Sub/Function. И никаких GoTo и меток операторов.

В начало статьи

"Теория суха, а древо жизни вечно зеленеет"

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


  i = 5
  GoTo Label
  ...
  For i = 1 To 10
    ... <Block1>
Label:
    ... <Block2>
  Next

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

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

Прежде всего, мне очень хотелось посмотреть фрагмент реальной программы, где подобная конструкция была бы нужна. Дело в том, что, увидев подобный код, сразу возникают серьезные сомнения — не напутал ли что-то его автор, в чем смысл этой конструкции? А смысл заключается в том, что в одном случае нужно в цикле выполнить два последовательных блока, а в другом — сначала одиночный Block2, а потом в цикле два последовательных. Ну, так давайте так и запишем:


If <условие> Then
  <Block1 при i = 5>
  Start = 6
Else
  Start = 1
End If

For i = Start To 10
    ... <Block1>
    ... <Block2>
Next

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

Однако давайте из соображений эффективности выберем вариант Калинина. Тщательно оттестируем его и убедимся в его работоспособности. Какие же опасности нас подстерегают?

Да, программа сейчас работает. А вы уверены, что она будет работать, если вы будете использовать новую версию компилятора или установите какой-нибудь Pentium 5? Поясню свой вопрос — вы точно знаете, в какой машинный код преобразуется конструкция For...Next?

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

Посмотрите, как выглядит код выполнения процедуры на ассемблере:


MyProc proc
       push bp
        ...
Label:    ; мы не выполнили формирование стека
       ...
       pop bp
       ret
MyProc endp

Нет нужды говорить о том, что прямая передача управления jmp Label из другой процедуры (операция вполне допустимая с точки зрения языка) приведет к краху системы.

Описанная угроза отнюдь не гипотетическая. Давайте посмотрим на такую конструкцию:


Do
  Dim nTotal As Long
  nTotal = nTotal + 1
Loop Until nTotal > 100
MsgBox nTotal    ' вывод сообщения

Так вот, в нынешней версии VB6 в результате выполнения данного кода вы получите "101". А, запустив программу в VB.NET, увидите сообщение об ошибке — "переменная nTotal не определена". Это произойдет потому, что в новой версии VB переменные, объявленные внутри блока, будут считаться локальными для данной конструкции.

Кстати, вот еще один пример быстрого, но некорректного кода:


For i = 1 To 10
  ...
Next i ' далее продолжаем обработку с текущим значением i

Все это очень здорово, но кто может гарантированно сказать, чему будет равно значение i при выходе из цикла, 10 или 11?

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


nEnd = SomeFunction(...)
For i = 1 To nEnd

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

Примеры опасностей можно продолжать, но, наверное, достаточно и этих.

В начало статьи

Главная задача программистов (раздел не попал в печатный вариант)

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

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

Летом прошлого года я получил письмо из Риги с просьбой помочь разобраться с проблемой. Максиму (так звали автора письма) нужно написать программу перекодировки на VBA для Word для преобразования текст (английский, русский, латышский) из DOS-овской кодировки в Unicode! Он сражается с этим уже несколько дней, сроки поджимают, программа у него на компьютере работает, а у заказчика (другая Windows, другие региональные установки) — нет.

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

Все это послужило началом нашей активной двухдневной переписке. Максим сообщил в ответ, что он достаточно опытный программист (Pascal, C/C++) со стажем 6 лет. Размер исходного кода его программы действительно довольно велик, но это потому (ВНИМАНИЕ!), что ему нужно было в минимальные сроки написать программу, которая бы работала быстрее.

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

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

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

В начало статьи

"Иногда лучше жевать, чем говорить"

Одна из заповедей преферанса гласит: "Пока не проиграешь 50 рублей (курс 60-х годов), играть не научишься". Обратите внимание — это условие необходимое, но не достаточное. Нужно не только играть и проигрывать деньги, но еще и изучать теорию преферанса.

В начало статьи

Приложения в отдельных файлах:

В начало статьи