Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 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). ГЛАВА 16 Циклы 379 полагает, что census — это структура, содержащая сведения о людях из рассчиты- ваемой группы. Следующий шаг — построение цикла вокруг существующих выражений. Поскольку цикл должен вычислять ставки для каждого человека из группы, индекс должен перечислять всех членов группы. Шаг 4: Создание цикла изнутри наружу (псевдокод) For person = firstPerson to lastPerson rate = table[ census.Age, census.Gender ] totalRate = totalRate + rate End For Все, что вы должны сделать, — это поместить цикл for вокруг существующего кода и добавить к нему пару begin-end. Напоследок убедитесь, что переменные, исполь- зующие индекс цикла person, написаны правильно. В данном случае переменная census изменяется вместе с person, поэтому ее следует корректно проиндексировать. Шаг 5: Создание цикла изнутри наружу (псевдокод) For person = firstPerson to lastPerson rate = table[ census[ person ].Age, census[ person ].Gender ] totalRate = totalRate + rate End For И, наконец, напишите необходимую инициализацию. В этом примере нужно ини- циализировать переменную totalRate. Последний шаг: Создание цикла изнутри наружу (псевдокод) totalRate = 0 For person = firstPerson to lastPerson rate = table[ census[ person ].Age, census[ person ].Gender ] totalRate = totalRate + rate End For Если вы хотите добавить еще один цикл вокруг цикла person, продолжайте таким же образом. Вы не должны жестко придерживаться этого порядка. Идея в том, чтобы начать с чего-то определенного, думать только об одной задаче в каждый момент времени и строить цикл из простых компонентов. Предпринимайте маленькие, понятные шаги, постепенно обобщая и усложняя цикл. Таким образом, вы мини- мизируете количество кода, на котором необходимо одновременно сосредоточи- ваться и, следовательно, уменьшите вероятность ошибки. 16.4. Соответствие между циклами и массивами Циклы и массивы часто связаны друг с другом. Зачастую цикл создается для манипуляций с массивами, и счетчики цикла один к одному соответствуют индексам массива. Так, следу- ющие индексы циклов for соответствуют индексам массива: Перекрестная ссылка О соответ- ствии между циклами и масси- вами см. также раздел 10.7. 380 ЧАСТЬ IV Операторы Пример умножения массивов (Java) for ( int row = 0; row < maxRows; row++ ) { for ( int column = 0; column < maxCols; column++ ) { product[ row ][ column ] = a[ row ][ column ] * b[ row ][ column ]; } } В языке Java цикл для таких операций с массивами необходим. Но стоит заметить, что циклические структуры и массивы не обязательно должны использоваться вместе. Некоторые языки, особенно APL и Fortran 90 и более поздние, предостав- ляют операции с массивами, исключающие необходимость применять такие циклы, как только что продемонстрированные. Вот так выглядит фрагмент кода на APL, выполняющий ту же операцию: Пример умножения массивов (APL) product <- a x b Вариант на APL проще и менее подвержен ошибкам. Он использует только три операнда, тогда как фрагмент на Java — 17. Он не содержит переменных цикла, индексов массива или управляющих структур, которые можно некорректно зако- дировать. Из этих примеров следует, что частично программирование направлено на ре- шение задачи, а частично — на решение этой задачи на определенном языке. Выбранный вами язык существенно влияет на получаемый результат. Контрольный список: циклы Выбор и создание цикла Используется ли цикл while вместо цикла for, если он больше подходит? Создавался ли цикл изнутри наружу? Вход в цикл Выполняется ли вход в цикл сверху? Расположен ли код инициализации непосредственно перед циклом? Если необходим бесконечный или событийный цикл, конструируется ли он явно, или сделан такой ляп, как for i = 1 to 9999? В цикле for в C++, C или Java резервируется ли заголовок цикла только для управляющего кода? Тело цикла Использует ли цикл скобки { и } или их эквиваленты для обрамления тела цикла и предотвращения проблем, связанных с неправильной модификацией? Содержит ли тело цикла хоть что-то? Не пустое ли оно? Сгруппированы ли служебные операции в начале или конце цикла? Выполняет ли цикл одну и только одну функцию, как это делает хорошо спроектированный метод? Достаточно ли цикл короткий, чтобы его можно было сразу увидеть целиком? Не превышает ли вложенность цикла трех уровней? http://cc2e.com/1616 |