Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 17 Нестандартные управляющие структуры 389 Пример неправильного решения: вычисления факториала с помощью рекурсии (Java) int Factorial( int number ) { if ( number == 1 ) { return 1; } else { return number * Factorial( number 1 ); } } Не считая медленного выполнения и непредсказуемого использования памяти, рекурсивный вариант функции трудней для понимания, чем итеративный вариант: Пример правильного решения: использование итераций для вычисления факториала (Java:) int Factorial( int number ) { int intermediateResult = 1; for ( int factor = 2; factor <= number; factor++ ) { intermediateResult = intermediateResult * factor; } return intermediateResult; } Из этого примера можно усвоить три урока. Первый: учебники по ВычТеху не оказывают миру услугу своими примерами рекурсии. Второй, и более важный: рекурсия — гораздо более мощный инструмент, чем можно предположить из сби# вающих с толку примеров расчета факториала и чисел Фибоначчи. Третий — са# мый важный: вы должны рассмотреть альтернативные варианты перед использо# ванием рекурсии. Иногда один способ работает лучше, иногда — другой. Но прежде чем выбрать какой#то один, надо рассмотреть оба. 17.3. Оператор goto Вы могли думать, что дебаты вокруг goto утихли, но корот# кая экскурсия по современным репозиториям исходного кода, таким как SourceForge.net, показывает, что goto все еще жив# здоров и глубоко укоренился на сервере вашей компании. Более того, современ# ные эквиваленты обсуждения goto до сих пор возникают под разными личинами, включая дебаты о множественных возвратах из методов, множественных выходах из цикла, именованных выходах из цикла, обработке ошибок и исключений. Аргументы против goto Основной аргумент против goto состоит в том, что код без goto — более качествен# ный. Знаменитое письмо Дейкстры «Go To Statement Considered Harmful» («Обо# снование пагубности оператора go to») в мартовском номере «Communications of the ACM» 1968 г. положило начало дискуссии. Дейкстра отметил, что качество кода http://cc2e.com/1785 390 ЧАСТЬ IV Операторы обратно пропорционально количеству goto, использованных программистом. В последующих работах Дейкстра утверждал, что корректность кода, не содержа# щего goto, доказать легче. Код с операторами goto трудно форматировать. Для демонстрации логической структуры используются отступы, а goto влияет на логическую структуру. Однако использовать отступы, чтобы показать логику goto и места его перехода, сложно или даже невозможно. Применение goto препятствует оптимизации, выполняемой компилятором. Неко# торые виды оптимизации зависят от порядка выполнения нескольких выражений подряд. Безусловный переход goto усложняет анализ кода и уменьшает возмож# ность оптимизации кода компилятором. Таким образом, даже если применение goto увеличивает эффективность на уровне исходного кода, суммарный эффект из#за невозможности оптимизации может уменьшиться. Сторонники операторов goto иногда приводят довод, что они делают программу быстрее и проще. Но код, содержащий goto, обычно не самый быстрый и корот# кий из всех возможных. Изумительная классическая статья Дональда Кнута «Struc# tured Programming with go to Statements» («Структурное программирование и операторы go to») содержит несколько примеров, в которых применение goto приводит к более медленному и объемному коду (Knuth, 1974). На практике применение операторов goto приводит к нарушению принципа, что нормальный ход алгоритма должен быть строго сверху вниз. Даже если goto при аккуратном использовании не сбивают с толку, как только они появляются, они начинают распространяться по коду, как термиты по разрушающемуся дому. Если разрешен хотя бы один goto, вместе с пользой в код проникает и вред, так что лучше вообще запретить использование этого оператора. В целом опыт двух десятилетий, прошедших с публикации письма Дейкстры по# казал всю недальновидность создания кода, перегруженного операторами goto. В своем обзоре литературы Бен Шнейдерман (Ben Shneiderman) сделал вывод, что факты свидетельствуют в пользу Дейкстры и нам лучше обходиться без goto (1980), а многие современные языки, включая Java, даже не содержат такой оператор. Аргументы в защиту goto Сторонники goto ратуют за осторожное применение оператора при определенных обстоятельствах, а не за неразборчивое употребление. Большинство аргументов против goto говорит именно о неразборчивом его использовании. Дискуссия о goto вспыхнула, когда Fortran был наиболее популярным языком. Fortran не имел при# личных циклов, и в отсутствие хорошего совета по поводу создания цикла с помо# щью goto программисты написали кучу спагетти#кода. Такой код, несомненно, кор# релировал с выпуском низкокачественных программ, но это имело отдаленное отношение к аккуратному использованию goto, позволяющему заполнить пробел в возможностях, предоставляемых современными языками программирования. Правильно расположенный goto способен помочь избавиться от дублирования кода. Такой код создает проблемы, если две его части модифицируются по#разному. Дуб# лированный код увеличивает размер исходного и выполняемого файлов. Отри# ГЛАВА 17 Нестандартные управляющие структуры 391 цательный эффект применения goto перевешивается недостатками дублирован# ного кода. Оператор goto может пригодиться в методе, который сна# чала распределяет ресурсы, выполняет с ними какие#то опе# рации, а потом освобождает эти ресурсы. Используя goto, вы можете выполнять очистку в одном месте. Оператор goto уменьшает вероятность того, что вы забудете освободить ресурсы при обнаружении ошибки. Порой goto позволяет создать более быстрый и короткий код. Вышеупомянутая статья Кнута 1974 года рассматривает несколько вариантов, в которых goto дает ощутимое преимущество. Хорошее программирование не означает исключение всех goto. Систематическая декомпозиция, усовершенствование и разумный выбор управляющих структур обычно автоматически приводит к программам, не содержащим goto. Стремление к коду без goto — это не цель, а результат, и бесполезно заострять внимание ис# ключительно на устранении goto. Десятилетия исследований операторов goto не смогли про# демонстрировать их вредоносность. В обзоре литературы Б.А. Шейл (B.A. Sheil) сделал вывод, что нереалистичные тес# товые условия, плохой анализ данных и неубедительные ре# зультаты не подкрепляют заявления Шнейдермана и др., что число ошибок в коде пропорционально количеству goto (1981). Шейл не зашел так далеко, чтобы утверждать, что ис# пользование goto — хорошая идея, он лишь показал, что эк# спериментальные данные против этих операторов неубеди# тельны. И, наконец, операторы goto входят во множество современ# ных языков, включая Visual Basic, C++ и Ada — наиболее тщательно продуманный язык программирования в истории. Ada создавался уже после того, как были при# ведены все аргументы с обеих сторон дискуссии по goto, и после всестороннего рассмотрения вопроса разработчики Ada решили включить в него goto. Воображаемая дискуссия по поводу goto Отличительная особенность большинства обсуждений goto — поверхностность. Спорщик, утверждающий, что « goto — это зло», приводит тривиальный фрагмент кода, содержащий операторы goto, а затем показывает, как легко его можно пере# писать без goto. Это доказывает главным образом то, что тривиальный код можно легко написать и без goto. Спорщик, утверждающий: «Я не могу жить без goto», — обычно приводит случай, в котором исключение goto выливается в дополнительное сравнение или дубли# рование кода. Это доказывает в основном то, что есть случаи, в которых goto по# зволяет выполнить на одно сравнение меньше — незначительная выгода для со# временных компьютеров. Перекрестная ссылка О примене- нии операторов goto в коде, ис- пользующем ресурсы, см. ниже подраздел «Обработка ошибок и операторы goto». Об исключениях см. также раздел 8.4. Факты свидетельствуют лишь о том, что намеренно хаотичная управляющая структура ухудша- ет производительность [програм- миста]. Эти эксперименты не предоставили практически ни- какого доказательства полезно- го эффекта какого-то конкрет- ного способа структурирования управляющей логики. Б.А. Шейл 392 ЧАСТЬ IV Операторы Большинство учебников также не помогает. Они приводят простой пример пере# писывания некоторого кода без goto, как будто это все объясняет. Вот обманчи# вый пример тривиального фрагмента кода из такого учебника: Пример кода, который должен легко переписываться без goto (C++) do { GetData( inputFile, data ); if ( eof( inputFile ) ) { goto LOOP_EXIT; } DoSomething( data ); } while ( data != 1 ); LOOP_EXIT: Книга быстро заменяет этот фрагмент кодом без goto: Пример предположительно эквивалентного кода, переписанного без goto (C++) GetData( inputFile, data ); while ( ( !eof( inputFile ) ) && ( ( data != 1 ) ) ) { DoSomething( data ); GetData( inputFile, data ) } Этот так называемый «простой» пример содержит ошибку. В случае, когда пере# менная data равна %1, преобразованный код отслеживает %1 и выходит из цикла до выполнения DoSomething(). Исходный код выполняет DoSomething() до того, как %1 обнаружена. Автор книги по программированию, пытаясь показать, как легко можно кодировать без goto, преобразовал собственный же пример некорректно. Но ему не стоит расстраиваться — другие книги содержат похожие ошибки. Даже профессионалы сталкиваются с трудностями при преобразовании кода, исполь# зующего goto. Вот более точная реорганизация кода без goto: Пример действительно эквивалентного кода, переписанного без goto (C++) do { GetData( inputFile, data ); if ( !eof( inputFile )) { DoSomething( data ); } } while ( ( data != 1 ) && ( !eof( inputFile ) ) ); Даже при правильном преобразовании кода этот пример все же искусственный, потому что он показывает тривиальный вариант использования goto. Это не тот случай, когда толковые программисты выбирают goto в качестве предпочтитель# ной формы управления. В наши дни уже тяжело добавить что#нибудь стоящее к теоретическим дебатам вокруг goto. Однако на что обычно не обращают внимания, так это на ситуации, в кото# ГЛАВА 17 Нестандартные управляющие структуры 393 рых программист, полностью представляя себе альтернативы без goto, все же ре# шает использовать его для улучшения читабельности и качества сопровождения. Следующие разделы представляют случаи, в которых некоторые опытные програм# мисты приводят доводы в пользу goto. В обсуждении рассматриваются примеры кода с операторами goto и кода, переписанного без их использования, и оцени# ваются достоинства и недостатки этих версий. Обработка ошибок и операторы goto Создание высокоинтерактивного кода заставляет обращать особое внимание на обработку ошибок и освобождение ресурсов в случае возникновения ошибки. Следующий пример стирает группу файлов. Метод сначала получает группу фай# лов для удаления, затем находит каждый файл, открывает его, перезаписывает, а затем удаляет. Метод проверяет возникновение ошибок на каждом шаге. Пример кода с goto, который обрабатывает ошибки и освобождает ресурсы (Visual Basic) ‘ Этот метод стирает группу файлов. Sub PurgeFiles( ByRef errorState As Error_Code ) Dim fileIndex As Integer Dim fileToPurge As Data_File Dim fileList As File_List Dim numFilesToPurge As Integer MakePurgeFileList( fileList, numFilesToPurge ) errorState = FileStatus_Success fileIndex = 0 While ( fileIndex < numFilesToPurge ) fileIndex = fileIndex + 1 If Not ( FindFile( fileList( fileIndex ), fileToPurge ) ) Then errorState = FileStatus_FileFindError Здесь используется GoTo. GoTo END_PROC End If If Not OpenFile( fileToPurge ) Then errorState = FileStatus_FileOpenError Здесь используется GoTo. GoTo END_PROC End If If Not OverwriteFile( fileToPurge ) Then errorState = FileStatus_FileOverwriteError > > 394 ЧАСТЬ IV Операторы Здесь используется GoTo. GoTo END_PROC End If if Not Erase( fileToPurge ) Then errorState = FileStatus_FileEraseError Здесь используется GoTo. GoTo END_PROC End If Wend Здесь находится метка GoTo. END_PROC: DeletePurgeFileList( fileList, numFilesToPurge ) End Sub Этот метод — типичный пример обстоятельств, при которых опытные програм# мисты решают использовать goto. Похожее случается, когда методу надо выделить и освободить такие ресурсы, как соединения с базами данных, память или вре# менные файлы. Альтернативой goto в таких ситуациях обычно является дублиро# вание кода для очистки ресурсов. В подобных случаях программист может срав# нить нежелательность применения goto с головной болью от сопровождения дуб# лированного кода и решить, что goto — меньшее зло. Вы можете переписать предыдущий пример без goto несколькими способами, и все они будут иметь как плюсы, так и минусы. Далее приведены возможные стра# тегии преобразования: Переписать с помощью вложенных операторов if При перезаписи с помощью вложенных if располагайте блоки if так, чтобы следующая проверка условия выполнялась, только если предыдущая завершилась успешно. Это стандартный, приводимый в учебниках подход к удалению операторов goto. Рассмотрим метод, переписанный с помощью стандарт# ного подхода: Код, избавившийся от goto с помощью вложенных if (Visual Basic) ‘ Этот метод стирает группу файлов. Sub PurgeFiles( ByRef errorState As Error_Code ) Dim fileIndex As Integer Dim fileToPurge As Data_File Dim fileList As File_List Dim numFilesToPurge As Integer MakePurgeFileList( fileList, numFilesToPurge ) errorState = FileStatus_Success fileIndex = 0 Перекрестная ссылка Этот ме- тод также можно переписать, используя операторы break и без goto. Об этом подходе см. подраздел «Досрочное заверше- ние цикла» раздела 16.2. > > > ГЛАВА 17 Нестандартные управляющие структуры 395 Условие While изменено — добавлена проверка errorState. While ( fileIndex < numFilesToPurge And errorState = FileStatus_Success ) fileIndex = fileIndex + 1 If FindFile( fileList( fileIndex ), fileToPurge ) Then If OpenFile( fileToPurge ) Then If OverwriteFile( fileToPurge ) Then If Not Erase( fileToPurge ) Then errorState = FileStatus_FileEraseError End If Else ‘ невозможно перезаписать файл errorState = FileStatus_FileOverwriteError End If Else ‘ невозможно открыть файл errorState = FileStatus_FileOpenError End If Else ‘ файл не найден Эта строка расположена через 13 строк после условия If, к которому она относится. errorState = FileStatus_FileFindError End If Wend DeletePurgeFileList( fileList, numFilesToPurge ) End Sub Тому, кто привык программировать без goto, возможно, будет легче читать этот код, чем первоначальную версию. И если вы используете данный вариант, вам не придется предстать перед судом противников goto. Основной недостаток этого подхода с вложенными if в том, что уровень вложенности глубок, даже слишком. Для пони# мания кода вам нужно держать в голове весь набор вложен# ных if одновременно. Более того, расстояние между кодом обработки ошибок и кодом, ее инициирующим, слишком велико: например, выражение, присваивающее переменной errorState значение FileStatus_FileFindError, на 13 строк отстоит от соответствующей проверки if. В варианте с goto ни одно выражение не отстоит более чем на четыре строки от условия, которое его вызывает. И вам нет нужды держать в голове всю структуру одновременно. По сути вы можете игнорировать все предыдущие условия, выпол# ненные успешно, и сосредоточиться на следующей операции. В этом случае вер# сия с goto гораздо удобнее для чтения и сопровождения, чем с вложенными if. Переписать код с использованием статусной переменной Чтобы перепи# сать код с использованием статусной переменной (также называемой перемен# ной состояния), создайте переменную, которая будет показывать, не находится ли метод в состоянии ошибки. В нашем случае метод уже содержит статусную пере# менную errorState, так что вы можете использовать ее. Перекрестная ссылка Об отсту- пах и других вопросах размет- ки кода см. главу 31. Об уров- нях вложенности см. раздел 19.4. > > 396 ЧАСТЬ IV Операторы Код, избавившийся от goto с помощью статусной переменной (Visual Basic) ‘ Этот метод стирает группу файлов. Sub PurgeFiles( ByRef errorState As Error_Code ) Dim fileIndex As Integer Dim fileToPurge As Data_File Dim fileList As File_List Dim numFilesToPurge As Integer MakePurgeFileList( fileList, numFilesToPurge ) errorState = FileStatus_Success fileIndex = 0 Условие While изменено — добавлена проверка errorState. While ( fileIndex < numFilesToPurge ) And ( errorState = FileStatus_Success ) fileIndex = fileIndex + 1 If Not FindFile( fileList( fileIndex ), fileToPurge ) Then errorState = FileStatus_FileFindError End If Проверяется статусная переменная. If ( errorState = FileStatus_Success ) Then If Not OpenFile( fileToPurge ) Then errorState = FileStatus_FileOpenError End If End If Проверяется статусная переменная. If ( errorState = FileStatus_Success ) Then If Not OverwriteFile( fileToPurge ) Then errorState = FileStatus_FileOverwriteError End If End If Проверяется статусная переменная. If ( errorState = FileStatus_Success ) Then If Not Erase( fileToPurge ) Then errorState = FileStatus_FileEraseError End If End If Wend DeletePurgeFileList( fileList, numFilesToPurge ) End Sub Преимущество подхода со статусной переменной в том, что он позволяет избе# жать глубоко вложенных структур if%then%else, используемых в предыдущем при# мере, и тем самым легче для понимания. Кроме того, он помещает действия, сле# > > > > |