Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 16 Циклы 371 // Много кода return( found ); Этот второй фрагмент использует дополнительную переменную и располагает обращения к recordCount в более ограниченном пространстве. Как часто бывает при применении вспомогательной логической переменной, результирующий код становится яснее. Рассмотрите использование счетчиков безопасности Счетчик безопасно# сти — это переменная, увеличивающаяся при каждом проходе цикла, чтобы определить, не слишком ли много раз выполняется цикл. Если вы пишете програм# му, в которой любая ошибка будет катастрофической, вы можете использовать счетчики безопасности, чтобы убедиться, что все циклы заканчиваются. Такой цикл на C++ вполне может использовать счетчик безопасности: Пример цикла, который мог бы использовать счетчик безопасности (C++) do { node = node>Next; } while ( node>Next != NULL ); Вот тот же код с добавленным счетчиком безопасности: Пример использования счетчика безопасности (C++) safetyCounter = 0; do { node = node>Next; Здесь код счетчика безопасности. safetyCounter++; if ( safetyCounter >= SAFETY_LIMIT ) { Assert( false, “Internal Error: SafetyCounter Violation.” ); } } while ( node>Next != NULL ); Счетчики безопасности не панацея. Добавляемые в код по одному, они увеличи# вают сложность и могут привести к дополнительным ошибкам. Так как они не при# меняются в каждом цикле, вы можете забыть поддержать код счетчика при моди# фикации циклов в той части программы, где они все же используются. Но если счетчики безопасности вводятся на уровне проектного стандарта для критичес# ких циклов, вы будете ожидать их, и код этих счетчиков будет не более подвер# жен ошибкам, чем любой другой. Досрочное завершение цикла Многие языки предоставляют средства для завершения цикла без выполнения условий for или while. В данном обсуждении слово break обозначает общий тер# > 372 ЧАСТЬ IV Операторы мин для оператора break в C++, C и Java; выражений Exit%Do и Exit%For в Visual Basic и подобных конструкций, включая имитации с помощью goto, в языках, не под# держивающих break напрямую. Оператор break (или его эквивалент) приводит к завершению цикла через нормальный канал выхода. Программа продолжает вы# полнение с первого оператора, расположенного после цикла. Оператор continue похож на break в том смысле, что это вспомогательное сред# ство для управления циклом. Однако вместо выхода из цикла, continue заставляет программу пропустить тело цикла и продолжить выполнение со следующей ите# рации. Оператор continue — это сокращенный вариант блока if%then, предотвра# щающего выполнение остальной части цикла. Рассмотрите использование операторов break вместо логических фла' гов в цикле while Порой добавление логических флагов в цикл while с целью имитации выхода из тела цикла усложняет чтение кода. Иногда вы можете убрать несколько уровней отступа в цикле и упростить его управление, просто исполь# зуя break вместо группы проверок if. Размещение нескольких отдельных условий break рядом с кодом, приводящим к их выполнению, может уменьшить вложен# ность и сделать цикл читабельнее. Остерегайтесь цикла с множеством операторов break, разбросанных по всему коду Цикл, содержащий большое количество операторов break, может сиг# нализировать о нечетком представлении структуры цикла или его роли в окру# жающем коде. Рост числа break увеличивает вероятность, что цикл может быть более ясно представлен в виде набора нескольких циклов вместо одного цикла с мно# жеством выходов. Согласно статье в «Software Engineering Notes» программная ошибка, которая 15 января 1990 года на 9 часов вывела из строя телефонную сеть Нью#Йорка, воз# никла благодаря лишнему оператору break.(SEN, 1990): Пример ошибочного использования оператора break в блоке do-switch-if (C++) do { switch if () { Этот break предназначался для if, но вместо этого привел к выходу из switch. break; } } while ( ... ); Большое количество break не обязательно означает ошибку, но их присутствие в цикле — тревожный сигнал: как канарейка в шахте, задыхающаяся из#за недостатка воздуха, вместо того чтобы петь. > ГЛАВА 16 Циклы 373 Используйте continue для проверок в начале цикла Хорошим применени# ем оператора continue будет перемещение операций в конец тела цикла после про# верки некоторого условия в его начале. Например, если цикл читает записи, от# брасывает часть из них, а остальные обрабатывает, вы можете поместить подоб# ную проверку в начало цикла: Пример относительно безопасного использования continue (псевдокод) while ( not eof( file ) ) do read( record, file ) if ( record.Type <> targetType ) then continue — Обрабатываем запись targetType. end while Такое использование continue позволяет избегать проверок if, что эффективно уменьшит отступы внутри всего тела цикла. С другой стороны, если continue воз# никает в середине или конце цикла, используйте вместо него if. Используйте структуру break с метками, если ваш язык ее поддержи' вает Java поддерживает помеченные операторы break, что позволяет предотв# ратить проблемы, приведшие к выходу из строя телефонов в Нью#Йорке. break с меткой можно использовать для выхода из цикла for, условия if или любого блока кода, заключенного в скобки (Arnold, Gosling and Holmes, 2000). Вот возможное решение «нью#йоркской проблемы», переписанное на Java вмес# то C++, что позволяет использовать break с меткой: Пример лучшего использования помеченного оператора break в блоке do-switch-if (Java) do { switch CALL_CENTER_DOWN: if () { Назначение помеченного break однозначно. break CALL_CENTER_DOWN; } } while ( ... ); Используйте операторы break и continue очень осторожно Применение break исключает возможность представления цикла в виде черного ящика. Если вы ограничиваетесь только одним выражением для управления условием выхода из цикла, то получаете мощное средство для упрощения циклов. Применение break > 374 ЧАСТЬ IV Операторы заставляет читателя смотреть внутрь цикла, чтобы разобраться в его управлении. Это усложняет понимание цикла. Используйте break только после того, как рассмотрели все альтернативы. Вы не можете сказать с уверенностью, хороши или плохи конструкции continue и break. Некоторые ученые утверждают, что это допустимые технологии в структурном программировании, а некоторые — что нет. Поскольку вы не знаете, правильно ли применять continue и break вообще, используйте их, но не забывайте, что вы можете быть неправы. На самом деле это сводится к простому утверждению: если вы не можете аргументировать применение break или continue, не применяя их. Проверка граничных точек При разработке цикла обычно представляют интерес три точки: первая итерация, случайно выбранная итерация в середине и последняя итерация. Когда вы созда# ете цикл, мысленно пройдитесь по этим трем точкам и убедитесь, что в цикле нет ошибки потери единицы. Если цикл содержит какие#то специальные случаи, вы# полнение которых отличается от первой или последней итерации, проверьте их тоже. Если цикл производит сложные вычисления, достаньте свой калькулятор и проверьте их вручную. Готовность выполнять такой вид проверки — ключевое различие между квалифицированными и неквалифицированными программистами. Пер# вые проделывают мысленное моделирование и вычисления вручную, потому что знают, что эти меры помогут им найти ошибки. Вторые имеют склонность к случайному экспериментированию, пока не найдут правдоподобную комбинацию. Если цикл не работает так, как предполагалось, неумелый программист меняет знак < на <=. Если и это не помогает, он исправ# ляет индекс цикла, добавляя или вычитая 1. В конечном счете таким способом программист может нащупать правильную комбинацию или просто заменить изначальную ошибку более незаметной. Даже если этот случайный процесс при# ведет к правильной программе, программист не будет знать, почему она работа# ет корректно. Мысленное моделирование и ручные вычисления могут дать несколько преиму# ществ. Умственная тренировка приводит к меньшему количеству ошибок при первоначальном кодировании, более быстрому обнаружению проблем при отладке и в целом более полному пониманию программы. Умственные упражнения озна# чают, что вы знаете, как работает код, а не просто предполагаете это. Использование переменных цикла Далее описаны некоторые принципы применения переменных цикла. Используйте порядковые или перечислимые типы для границ массивов и циклов Обычно счетчики циклов должны быть целыми значениями. Числа с плавающей за# пятой плохо инкрементируются. Например, вы можете при# бавить 1,0 к 26 742 897,0 и получить 26 742 897,0 вместо 26 742 898,0. Если это число используется как индекс цикла, вы получите беско# нечный цикл. Перекрестная ссылка Об имено- вании переменных цикла см. подраздел «Именование индек- сов циклов» раздела 11.2. ГЛАВА 16 Циклы 375 Используйте смысловые имена переменных, чтобы сделать вло' женные циклы читабельными Массивы часто индексируются с по# мощью тех же переменных, что используются как индексы цикла. Если у вас одномерный массив, то вы еще сможете выйти сухим их воды, применяя i, j или k для его индексации. Но если у массива два и более измерений, вам следует задавать значимые имена для индексов, чтобы прояснить свои действия. Смысло# вые имена индексов массивов одновременно уточняют и назначение цикла, и эле# мент массива, к которому вы планируете обратиться. Вот пример кода, который не применяет этот принцип: в нем использованы бес# смысленные имена i, j и k: Пример неправильных имен переменных цикла (Java) for ( int i = 0; i < numPayCodes; i++ ) { for ( int j = 0; j < 12; j++ ) { for ( int k = 0; k < numDivisions; k++ ) { sum = sum + transaction[ j ][ i ][ k ]; } } } Как вы думаете, что означают индексы в элементе transaction? Сообщают ли пе# ременные i, j и k что#либо о содержимом transaction? Если вы знаете объявление transaction, можете ли вы легко определить, указаны ли индексы в правильном порядке? Вот тот же цикл с более читабельными именами переменных: Пример хороших имен переменных цикла на Java for ( int payCodeIdx = 0; payCodeIdx < numPayCodes; payCodeIdx++ ) { for (int month = 0; month < 12; month++ ) { for ( int divisionIdx = 0; divisionIdx < numDivisions; divisionIdx++ ) { sum = sum + transaction[ month ][ payCodeIdx ][ divisionIdx ]; } } } Как вы думаете, что означают индексы в элементе transaction на этот раз? В этом случае ответ получить проще, потому что имена переменных payCodeIdx, month и divisionIdx гораздо красноречивее, чем i, j и k. Компьютер с одинаковой легко# стью прочитает обе версии цикла. Однако людям легче будет читать вторую вер# сию, чем первую, поэтому второй вариант лучше, поскольку ваша основная ауди# тория состоит из людей, а не из компьютеров. Используйте смысловые имена во избежание пересечения индексов При# вычное использование переменных i, j и k приводит к увеличению риска пересе# чения индексов — использованию одного и того же имени индекса для разных целей. Взгляните: 376 ЧАСТЬ IV Операторы Пример пересечения индексов (C++) i сначала используется здесь... for ( i = 0; i < numPayCodes; i++ ) { // много кода for ( j = 0; j < 12; j++ ) { // много кода ...а теперь здесь for ( i = 0; i < numDivisions; i++ ) { sum = sum + transaction[ j ][ i ][ k ]; } } } Применение i настолько привычно, что эта переменная используется в одной вложенной структуре дважды. Второй цикл for, управляемый i, конфликтует с пер# вым — это и есть пересечение индексов. Применение более значимых имен, чем i, j и k, предотвратило бы проблему. Вообще, если тело цикла содержит больше пары строк кода, или может вырасти, или входит в группу вложенных циклов, из# бегайте переменных i, j и k. Ограничивайте видимость переменных'индексов цикла самим циклом Пе# ресечение индексов цикла и другое применение индексов вне самих циклов — настолько важная проблема, что разработчики языка Ada решили сделать индек# сы цикла for недоступными вне цикла. Попытка использования переменной#ин# декса вне цикла for приводит к ошибке времени компиляции. C++ и Java в какой#то мере реализуют ту же идею — они позволяют объявлять индексы цикла в нем самом, но не требуют этого. Выше, в примере раздела «Из# бегайте писать код, зависящий от последнего значения индекса цикла», перемен# ная recordCount может быть объявлена внутри выражения for, что ограничит ее область видимости этим циклом: Пример объявления переменной-индекса цикла внутри цикла for (C++) for ( int recordCount = 0; recordCount < MAX_RECORDS; recordCount++ ) { // Циклический код, использующий recordCount. } В принципе эта методика должна позволять создавать код, повторно объявляю# щий переменную recordCount в нескольких циклах без риска неправильного ис# пользования двух разных recordCount. Такое применение позволило бы писать, например, такой код: Пример объявления переменных-индексов внутри циклов for и их (возможно!) безопасное повторное использование (C++) for ( int recordCount = 0; recordCount < MAX_RECORDS; recordCount++ ) { // Циклический код, использующий recordCount. } > > ГЛАВА 16 Циклы 377 // Промежуточный код. for ( int recordCount = 0; recordCount < MAX_RECORDS; recordCount++ ) { // Дополнительный циклический код, использующий другую переменную recordCount. } Такая методика полезна для документирования назначения переменной recordCount. Однако не полагайтесь на ваш компилятор в вопросе области видимости record% Count. В разделе 6.3.3.1 книги «The C++ Programming Language» (Stroustrup, 1997) говорится, что переменная recordCount должна иметь область видимости, ограни# ченную ее циклом. Но, проверив эту функциональность в трех разных компиля# торах C++, я получил три разных результата: первый компилятор сигнализировал о повторном объявлении переменной recordCount во втором цикле for и сгенерировал ошибку; второй компилятор допустил объявление переменной recordCount во втором цикле for, но разрешил ее использование вне первого цикла for; третий компилятор разрешил оба объявления переменных recordCount и не допустил использования ни одной из них за пределами циклов, где они объяв# лялись. Как это часто бывает с наиболее эзотерическими свойствами языка, реализации компиляторов могут различаться. Насколько длинным может быть цикл? Длина цикла может измеряться в строках кода или глубине вложенности. Делайте циклы достаточно короткими, чтобы их можно было увидеть сразу целиком Если вы обычно смотрите на циклы на вашем мониторе, а ваш монитор показывает 50 строк, то установите 50#строчное ограничение длины. Эксперты предложили ограничивать длину цикла одной страницей. Однако ког# да вы оцените преимущество создания простого кода, вы редко будете писать циклы длиннее 15 или 20 строк. Ограничивайте вложенность тремя уровнями Иссле# дования показали, что способность программистов разоб# раться в цикле существенно снижается, если уровень вло# женности превышает три уровня (Yourdon, 1986a). Если вам нужно большее чис# ло уровней, сделайте цикл короче (концептуально), вынеся его часть в отдельный метод или упростив управляющую структуру. Выделяйте внутреннюю часть длинных циклов в отдельные методы Ес# ли цикл хорошо спроектирован, то код внутри него часто можно выделить в один или несколько методов, которые будут вызываться из цикла. Делайте длинные циклы особенно ясными Длина увеличивает сложность. Если вы пишете короткий цикл, вы можете использовать более рискованные управля# ющие структуры, такие как break и continue, множественные выходы, сложные условия завершения и т.д. Если вы пишете более длинный цикл и проявляете хоть какую#то заботу о читателях, вы предусмотрите в цикле только один выход и сде# лаете условие выхода исключительно понятным. Перекрестная ссылка Об упроще- нии вложенности см. раздел 19.4. 378 ЧАСТЬ IV Операторы 16.3. Простое создание цикла — изнутри наружу Если у вас иногда возникают затруднения при кодировании сложного цикла (что бывает у большинства программистов), есть простой способ реализовать его с первого раза. Вот как это сделать. Начните с одного действия. Закодируйте его с помощью констант. Затем сделайте отступ, окружите его циклом и замените кон# станты индексами цикла или вычисляемыми выражениями. Добавьте еще один цикл, если он нужен, и замените другие константы. Повторите процесс нужное число раз. После этого добавьте код инициализации. Так как вы начали с одного дей# ствия и двигались в сторону его обобщения, то можете рассматривать этот про# цесс как кодирование изнутри наружу. Допустим, вы разрабатываете программу для страховой компании. Ставки для страхования жизни варьируются в зависимости от возраста и пола страхователя. Ваша задача — написать метод, вычисляющий общую страховую премию для группы лиц. Вам нужен цикл, который будет брать ставку для каждого челове# ка из списка и добавлять ее к общей сумме. Вот что нужно сделать. Во#первых, в комментариях напишите шаги, которые должно выполнять тело цикла. Легче записать, что необходимо сделать, когда вы не думаете о деталях синтакси# са, индексах цикла, массива и т. п. Шаг 1: Создание цикла изнутри наружу (псевдокод) — Получить ставку из таблицы. — Добавить ставку к общей сумме. Во#вторых, замените комментарии в теле цикла на код, насколько это возможно без фактического написания всего цикла. В данном случае возьмите ставку для одного лица и добавьте ее к сумме. Используйте реальные данные, а не абстракции. Шаг 2: Создание цикла изнутри наружу (псевдокод) table еще не использует индексов. rate = table[ ] totalRate = totalRate + rate Пример предполагает, что table — это массив, содержащий данные о ставках. Сначала вам не надо беспокоиться об индексах массива. rate — это переменная, в которой хранится ставка, выбранная из таблицы ставок. Соответственно totalRate — переменная, содержащая сумму всех ставок. Далее добавьте индексы к массиву table: Шаг 3: Создание цикла изнутри наружу (псевдокод) rate = table[ census.Age ][ census.Gender ] totalRate = totalRate + rate Доступ к элементам массива осуществляется в зависимости от возраста и пола, поэтому census.Age и census.Gender служат для индексации массива. Пример пред# > Перекрестная ссылка Кодирова- ние цикла изнутри наружу похо- же на ППП (см. главу 9). |