Файл: Руководство по стилю программирования и конструированию по.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.11.2023
Просмотров: 828
Скачиваний: 2
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
ГЛАВА 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.
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.