Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 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, используемых в предыдущем при- мере, и тем самым легче для понимания. Кроме того, он помещает действия, сле- > > > > ГЛАВА 17 Нестандартные управляющие структуры 397 дующие за проверкой if-then-else, ближе к месту самой проверки, чем в случае с вложенными if, и совсем не использует блоки else. Понимание версии с вложенными if требует некоторой умственной гимнастики. Вариант со статусной переменной легче для понимания, потому что лучше моде- лирует способ человеческого мышления. Вы ищете файл. Если все в порядке, вы открываете файл. Если все до сих пор в порядке, вы перезаписываете файл. Если все до сих пор в порядке… Недостаток этого подхода в том, что использование статусных переменных — не настолько распространенная практика, как хотелось бы. Подробно документируйте их применение, иначе некоторые программисты могут не понять, что вы имели в виду. В данном примере применение хорошо названных перечислимых типов оказывает существенную помощь. Переписать с помощью try-finally Некоторые языки, включая Visual Basic и Java, предоставляют конструкцию try-finally, которая может быть использована для очистки ресурсов в случае ошибки. Чтобы переписать пример, используя подход с try-finally, поместите код, который должен проверять возможные ошибки, в блок try, а код очистки — в блок finally. Блок try задает область обработки исключений, а finally выполняет любое осво- бождение ресурсов. Блок finally будет вызываться всегда независимо от того, бу- дет ли сгенерировано исключение и будет ли это исключение перехвачено в ме- тоде PurgeFiles(). Код, избавившийся от goto с помощью try-finally (Visual Basic) ‘ Этот метод стирает группу файлов. Исключения передаются вызывающей стороне. Sub PurgeFiles() Dim fileIndex As Integer Dim fileToPurge As Data_File Dim fileList As File_List Dim numFilesToPurge As Integer MakePurgeFileList( fileList, numFilesToPurge ) Try fileIndex = 0 While ( fileIndex < numFilesToPurge ) fileIndex = fileIndex + 1 FindFile( fileList( fileIndex ), fileToPurge ) OpenFile( fileToPurge ) OverwriteFile( fileToPurge ) Erase( fileToPurge ) Wend Finally DeletePurgeFileList( fileList, numFilesToPurge ) End Try End Sub Этот подход предполагает, что все вызовы функций в случае ошибки генерируют исключения, а не возвращают коды ошибок. 398 ЧАСТЬ IV Операторы Преимущество подхода с применением try-finally в том, что он проще, чем с goto и не использует goto. Кроме того, он позволяет избежать глубоко вложенных струк- тур if-then-else. Ограничением данного варианта с try-finally является то, что он должен быть последовательно реализован во всем коде. Если бы предыдущий пример был ча- стью программы, использующей коды ошибок наряду с исключениями, то коду исключения пришлось бы устанавливать код ошибки для всех возможных оши- бок, и это требование сделало бы фрагмент примерно таким же сложным, как и другие варианты. Сравнение рассмотренных подходов В защиту каждой из четырех приведенных методик есть что сказать. Подход с goto позволяет избежать глубокой вложен- ности и ненужных проверок, но, увы, он содержит goto. Под- ход с вложенными if позволяет обойтись без goto, но его глу- бокая вложенность преувеличивает картину логической слож- ности метода. Подход со статусной переменной избегает goto и глубокой вложенности, но добавляет дополнительные про- верки. И, наконец, подход с try-finally тоже позволяет избе- жать как goto, так и глубокой вложенности, но доступен не во всех языках. Вариант с try-finally наиболее предпочтителен в языках, предоставляющих такую конструкцию и в системах, еще не стандартизовавших какой-то иной подход. Если этот вариант невозможен, то подход со статусной переменной немного предпоч- тительнее, чем goto и вложенные if, так как он читабельнее и лучше моделирует задачу, однако это не делает его лучшим во всех ситуациях. Все эти методики работают хорошо, если последовательно применяются ко все- му коду проекта. Рассмотрите все плюсы и минусы, а затем примите решение на уровне проекта о том, какой подход предпочесть. Операторы goto и совместное использование кода в блоке else Одна из возможных ситуаций, в которой некоторые программисты захотят ис- пользовать goto, — это случай, когда у вас есть две проверки условия и блок else и вы хотите выполнить код одного из условий и блока else. Вот пример варианта, который может кого-нибудь подвигнуть к использованию goto: Пример совместного использования кода в блоке else с помощью goto (C++) if ( statusOk ) { if ( dataAvailable ) { importantVariable = x; goto MID_LOOP; } } Перекрестная ссылка Полный список методик, которые мож- но применять в аналогичных ситуациях, перечислен в подраз- деле «Сводка методик уменьше- ния глубины вложенности» раз- дела 19.4. |