Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 12 Основные типы данных 293 Пример логического условия, чье назначение понятно (Java) finished = ( ( elementIndex < 0 ) || ( MAX_ELEMENTS < elementIndex ) ); repeatedEntry = ( elementIndex == lastElementIndex ); if ( finished || repeatedEntry ) { } Используйте логические переменные для упрощения сложных условий Чтобы правильно закодировать сложное условие, часто приходится делать несколько попыток. Когда через некоторое время нужно модифицировать это условие, быва- ет сложно разобраться, что же оно проверяет. Логические переменные могут уп- ростить проверку. В предыдущем примере программа на самом деле проверяет два условия: завершено ли выполнение метода и выполняется ли этот метод повторно. Создав логические переменные finished и repeatedEntry, вы упрощаете if-проверку: теперь ее легче читать, легче изменять, и она меньше подвержена ошибкам. Вот другой пример сложного условия: Пример сложного условия (Visual Basic) If ( ( document.AtEndOfStream() ) And ( Not inputError ) ) And _ ( ( MIN_LINES <= lineCount ) And ( lineCount <= MAX_LINES ) ) And _ ( Not ErrorProcessing() ) Then ‘ делаем что-то End If Условие в примере достаточно запутанное, но все же часто встречающееся. Оно налагает на читателя тяжелую умственную нагрузку. Могу предположить, что вы даже не попытаетесь разобраться в этой if-проверке, а посмотрите и скажете: «Раз- берусь с этим позже, если и впрямь понадобится». Обратите внимание на эту мысль, ведь это абсолютно то же самое, что сделают другие люди, читая ваш код, содер- жащий подобные условия. А вот как переписать этот код, используя логические переменные, добавленные для упрощения условия: Пример упрощенного условия (Visual Basic) allDataRead = ( document.AtEndOfStream() ) And ( Not inputError ) legalLineCount = ( MIN_LINES <= lineCount ) And ( lineCount <= MAX_LINES ) Вот упрощенная проверка. If ( allDataRead ) And ( legalLineCount ) And ( Not ErrorProcessing() ) Then ‘ делаем что-то End If Вторая версия проще. Думаю, вы легко прочитаете логическое выражение в if- проверке. > 294 ЧАСТЬ III Переменные Создайте свой логический тип в случае необходимости Некоторые язы- ки, такие как C++, Java и Visual Basic, имеют предопределенный логический тип, другие — скажем, C — не имеют. В языках, подобных C, вы можете определить свой логический тип. В C это можно сделать так: Пример определения типа BOOLEAN с помощью обычного typedef на языке C typedef int BOOLEAN; Или можно это сделать, определив дополнительно значения true и false: Пример определения типа Boolean с помощью Enum на языке C enum Boolean { True=1, False=(!True) }; Объявление переменных как BOOLEAN, а не int делает их последующее использо- вание более очевидным, а вашу программу — самодокументируемой. 12.6. Перечислимые типы Перечислимым называется тип данных, который позволяет описать на естествен- ном языке каждый элемент класса или объекта. Перечислимые типы реализова- ны в C++ и Visual Basic и обычно используются, когда вы знаете все возможные значения переменной и хотите выразить их словами. Вот примеры перечисли- мых типов на Visual Basic: Примеры перечислимых типов (Visual Basic) Public Enum Color Color_Red Color_Green Color_Blue End Enum Public Enum Country Country_China Country_England Country_France Country_Germany Country_India Country_Japan Country_Usa End Enum Public Enum Output Output_Screen Output_Printer ГЛАВА 12 Основные типы данных 295 Output_File End Enum Перечислимые типы — мощная альтернатива устаревшим схемам, в которых вы подробно расписываете: «1 — это красный, 2 — зеленый, 3 — голубой…». В связи с этим можно предложить следующие принципы применения таких типов. Используйте перечислимые типы для читабельности Вместо выражений вроде: if chosenColor = 1 вы можете написать более читабельную фразу, например: if chosenColor = Color_Red Каждый раз, когда вы видите числовую константу, подумайте: не заменить ли ее перечислимым типом. Перечислимые типы особенно полезны для определения параметров методов. Кто поймет, что означают параметры этой функции? Пример вызова функции, в котором стоило бы использовать перечислимые типы (C++) int result = RetrievePayrollData( data, true, false, false, true ); А параметры в этом вызове функции гораздо понятнее: Пример вызова функции, использующей перечислимые типы для читабельности (C++) int result = RetrievePayrollData( data, EmploymentStatus_CurrentEmployee, PayrollType_Salaried, SavingsPlan_NoDeduction, MedicalCoverage_IncludeDependents ); Используйте перечислимые типы для надежности В некоторых языках (осо- бенно в Ada) перечислимые типы заставляют компилятор выполнять более тщатель- ную проверку типов, чем при работе с целыми значениями и константами. При использовании именованных констант компилятору неоткуда узнать, что единствен- ные возможные значения переменной — это Color_Red, Color_Green и Color_Blue. Компилятор не будет возражать, встретив выражения вроде color = Country_England или country = Output_Printer. Если вы используете перечислимые типы, то при объяв- лении переменной типа Color компилятор позволит присвоить ей только значе- ния Color_Red, Color_Green или Color_Blue. Используйте перечислимые типы для модифицируемости Перечислимые типы позволяют вам легко модифицировать код. Обнаружив пробел в своей схе- ме «1 — это красный, 2 — зеленый, 3 — голубой», придется пройти по коду и из- менить все вхождения 1, 2, 3 и т. д. А используя перечислимые типы, вы можете 296 ЧАСТЬ III Переменные продолжить добавление элементов в список, просто поместив их в определение типа и перекомпилировав программу. Используйте перечислимые типы как альтернативу логическим пере- менным Очень часто логической переменной недостаточно для представления необходимых значений. Представьте, что у вас есть метод, возвращающий True, если он успешно выполнил свою задачу, и False в противном случае. Позднее вы можете обнаружить, что на самом деле у вас есть два варианта False. Первый оз- начает, что задание не выполнено, но это повлияло только на сам метод; второй — что невыполнение задания привело к фатальной ошибке, которую надо передать в остальную часть программы. В этом случае перечислимый тип со значениями Status_Success, Status_Warning и Status_FatalError подходит больше, чем логический тип со значениями true и false. Эту схему очень легко расширять новыми вариан- тами успешного или неудачного выполнения. Проверяйте некорректные значения Когда вы используете перечислимый тип в условиях if или case, проверяйте появление недопустимых значений. В переклю- чателе case для перехвата неправильных значений применяется оператор else: Хороший пример проверки некорректных значений в перечислимом типе (Visual Basic) Select Case screenColor Case Color_Red Case Color_Blue Case Color_Green Здесь выполняется проверка неправильного значения. Case Else DisplayInternalError( False, “Internal Error 752: Invalid color.” ) End Select Настройте первый и последний элемент перечислимого типа для исполь- зования в качестве границ циклов Определение первого и последнего эле- мента в перечислении в виде Color_First, Color_Last, Country_First, Country_Last и т. д., позволяет писать циклы, проходящие по всем элементам типа. Вы настраи- ваете свой перечислимый тип, используя явные значения: Пример установки значений First и Last перечислимого типа (Visual Basic) Public Enum Country Country_First = 0 Country_China = 0 Country_England = 1 Country_France = 2 Country_Germany = 3 Country_India = 4 Country_Japan = 5 > ГЛАВА 12 Основные типы данных 297 Country_Usa = 6 Country_Last = 6 End Enum Теперь элементы Country_First и Country_Last могут служить как границы цикла: Хороший пример циклического вызова элементов перечисления (Visual Basic) ‘ Вычисляем курсы валют по отношению к долларам США. Dim usaCurrencyConversionRate( Country_Last ) As Single Dim iCountry As Country For iCountry = Country_First To Country_Last usaCurrencyConversionRate( iCountry ) = ConversionRate( Country_Usa, iCountry ) Next Зарезервируйте первый элемент перечислимого типа как недопустимый При объявлении перечислимого типа зарезервируйте его первый элемент в ка- честве недопустимого значения. Большинство компиляторов присваивает перво- му элементу перечисления значение 0. Объявление некорректным элемента, при- равненного 0, поможет выявить неправильно проинициализированные перемен- ные, так как они вероятнее всего будут равны 0, чем любому другому неправиль- ному значению. Вот как при этом подходе будет выглядеть перечислимый тип Country: Пример объявления первого элемента перечисления недопустимым (Visual Basic) Public Enum Country Country_InvalidFirst = 0 Country_First = 1 Country_China = 1 Country_England = 2 Country_France = 3 Country_Germany = 4 Country_India = 5 Country_Japan = 6 Country_Usa = 7 Country_Last = 7 End Enum Точно укажите в стандартах кодирования для проекта, как должны ис- пользоваться первый и последний элементы, и неукоснительно придержи- вайтесь этого Использование в перечислениях элементов InvalidFirst, First и Last может сделать объявления массивов и операторы циклов читабельнее. Но есть вероятность неразберихи в таких вопросах: с чего начинаются значимые элементы перечисления — с 0 или 1 и является ли допустимыми первый и последний эле- менты? Если используется эта технология, стандарты кодирования для проекта в целях уменьшения количества ошибок должны требовать применения элементов InvalidFirst, First и Last для всех без исключения перечислений. 298 ЧАСТЬ III Переменные Помните о подводных камнях в присваивании явных значений элементам перечисления Некоторые языки позволяют присваивать конкретные значения элементам перечисления: Пример явного присваивания значений элементам перечисления (C++) enum Color { Color_InvalidFirst = 0, Color_First = 1, Color_Red = 1, Color_Green = 2, Color_Blue = 4, Color_Black = 8, Color_Last = 8 }; Если в этом примере вы объявите переменную цикла типа Color и попробуете пройти по всем элементам Color, то этой переменной наряду с допустимыми зна- чениями 1, 2, 4 и 8 будут присваиваться и недопустимые — 3, 5, 6 и 7. Если ваш язык не поддерживает перечислимые типы Если в вашем языке нет перечислимых типов, их можно имитировать, используя глобальные переменные или клас- сы. Например, такие объявления можно использовать в языке Java: Пример имитации перечислимых типов (Java) // Создание перечислимого типа Country. class Country { private Country() {} public static final Country China = new Country(); public static final Country England = new Country(); public static final Country France = new Country(); public static final Country Germany = new Country(); public static final Country India = new Country(); public static final Country Japan = new Country(); } // Создание перечислимого типа Output. class Output { private Output() {} public static final Output Screen = new Output(); public static final Output Printer = new Output(); public static final Output File = new Output(); } Перекрестная ссылка На момент написания этой книги Java не поддерживает перечислимых ти- пов. Но, возможно, уже делает это к моменту, когда вы это чи- таете. ГЛАВА 12 Основные типы данных 299 Такие перечислимые типы делают вашу программу читабельнее, поскольку вы можете использовать открытые члены класса, например, Country. England и Out- put. Screen, вместо именованных констант. Именно такой способ создания пере- числимых типов еще и обеспечивает безопасность типов, так как каждый тип объяв- лен как класс и компилятор будет проверять некорректные присваивания вроде Output output = Country. England (Bloch, 2001). В языках, не поддерживающих классы, такого же эффекта можно достичь, акку- ратно используя глобальные переменные для каждого элемента перечисления. 12.7. Именованные константы Именованные константы аналогичны переменным за исключением того, что вы не можете изменить значение константы после ее инициализации. Именованные кон- станты позволяют ссылаться на постоянные величины, например, максимальное количество работников, используя имя, а не число — MAXIMUM_EMPLOYEES, а не 1000. Применение именованных констант — это способ «параметризации» программы, т. е. размещение некоторой характеристики, которая может измениться, в пара- метре. В результате вам потребуется изменить его только в одном месте, а не по всей программе. Если вы когда-нибудь объявляли массив такой длины, какой, по- вашему, должно хватить для любых ситуаций, а потом выяснялось, что массив слишком мал, вы оцените значение именованных констант. Когда размер масси- ва изменяется, вы меняете только определение константы, используемой для объяв- ления массива. Такое «централизованное управление» имеет большое значение для того, чтобы сделать работу с ПО действительно приятной: упростить его поддер- жку и модификацию. Используйте именованные константы в объявлениях данных Именован- ные константы повышают читабельность и удобство сопровождения объявлений данных и выражений, которым необходимо знать размеры обрабатываемых дан- ных. В следующем примере для описания длины телефонных номеров работни- ков лучше использовать LOCAL_NUMBER_LENGTH, а не число 7. Хороший пример применения именованных констант в объявлениях данных (Visual Basic) Const AREA_CODE_LENGTH = 3 LOCAL_NUMBER_LEN GTH здесь объявляется как константа. Const LOCAL_NUMBER_LENGTH = 7 Type PHONE_NUMBER areaCode( AREA_CODE_LENGTH ) As String А здесь используется. localNumber( LOCAL_NUMBER_LENGTH ) As String End Type > > 300 ЧАСТЬ III Переменные ’ Убедимся, что все символы в телефонном номере — это цифры. И здесь тоже используется. For iDigit = 1 To LOCAL_NUMBER_LENGTH If ( phoneNumber.localNumber( iDigit ) < “0” ) Or _ ( “9” < phoneNumber.localNumber( iDigit ) ) Then ‘ Выполняем обработку ошибок. Это простой пример, но вы вполне можете представить программу, в которой сведения о длине телефонных номеров требуются во многих местах. На момент создания программы все работники живут в одной стране, поэтому вам нужно только семь цифр для их телефонных номеров. По мере расширения ком- пании ее филиалы открываются в разных странах, и вам понадобятся более длин- ные телефонные номера. Если вы параметризовали эту длину, вам надо сделать изменение только в одном месте — в определении именованной константы LOCAL_ NUMBER_LENGTH. Как вы, наверное, поняли, именованные константы делают сопровождение программы удобнее. Как правило, любая технология, централизующая управление объектами, подвер- женными изменениям, — это хороший способ уменьшить затраты на сопровождение (Glass, 1991). Избегайте литеральных значений, даже «безопасных» Как вы думаете, что в следующем цикле означает число 12? Пример непонятного кода (Visual Basic) For i = 1 To 12 profit( i ) = revenue( i ) – expense( i ) Next Исходя из специфического содержимого кода, можно предположить, что выпол- няется цикл по 12 месяцам в году. Но вы уверены? Вы поставите на это свое со- брание «Монти Пайтон»? В этом случае вам не нужно использовать именованные константы для поддерж- ки расширяемости: вряд ли число месяцев в году изменится в ближайшем буду- щем. Но если при написании кода остается хотя бы тень сомнения в его предназ- начении, развейте ее с помощью хорошо названной именованной константы, например, так: Пример более понятного кода (Visual Basic) For i = 1 To NUM_MONTHS_IN_YEAR profit( i ) = revenue( i ) – expense( i ) Next Это уже лучше, но для завершения примера индекс цикла тоже нужно назвать более информативно: Дополнительные сведения О зна- чении централизованного управ- ления см. стр. 57–60 в книге «Software Conflict» (Glass, 1991). > ГЛАВА 12 Основные типы данных 301 Пример еще более понятного кода (Visual Basic) For month = 1 To NUM_MONTHS_IN_YEAR profit( month ) = revenue( month ) – expense( month ) Next Этот пример выглядит весьма неплохо, но мы можем сделать еще один шаг впе- ред, применив перечислимый тип: Пример очень понятного кода (Visual Basic) For month = Month_January To Month_December profit( month ) = revenue( month ) – expense( month ) Next В последнем примере не может возникнуть никаких сомнений относительно назначения цикла. Даже если вы считаете, что литеральное значение безопасно, используйте вместо него именованную константу. Фанатично искорените лите- ралы из вашего кода. С помощью текстового редактора выполните поиск 2, 3, 4, 5, 6, 7, 8 и 9, чтобы убедиться, что вы не используете их случайно. Имитируйте именованные константы с помощью переменных или классов правильной области види- мости Если ваш язык не поддерживает именованные кон- станты, их можно создать. Подход, аналогичный приведен- ному выше Java-примеру, имитирующему перечислимые типы, позволяет получить преимущества использования именованных констант. Старайтесь применять обычные правила области види- мости: отдавайте предпочтение локальной, классовой или глобальной области видимости именно в таком порядке. Последовательно используйте именованные константы Опасно исполь- зовать для представления одной сущности именованные константы в одном мес- те и литералы в другом. Некоторые приемы программирования напрашиваются на ошибки, а этот просто доставляет вам ошибки на дом. Если значение имено- ванной константы нужно изменить, вы сделаете это и подумаете, что выполнили все необходимые изменения. Вы не обратите внимания на жестко закодирован- ные литералы, и ваша программа будет демонстрировать таинственные дефекты. Их устранение может потребовать так много усилий, что захочется схватить те- лефонную трубку и молить о помощи. 12.8. Массивы Массивы — простейшие и наиболее часто используемые типы структурирован- ных данных. В некоторых языках это единственный вид структурированных дан- ных. Массивы состоят из группы элементов одинакового типа, доступ к которым осуществляется напрямую по индексу. Убедитесь, что все значения индексов массива не выходят за его границы Все проблемы с массивами так или иначе связаны с тем, что доступ к их элементам может осуществляться произвольно. Наиболее часто Перекрестная ссылка Об ими- тации перечислимых типов см. подраздел «Если ваш язык не поддерживает перечислимые типы» раздела 12.6. 302 ЧАСТЬ III Переменные возникающая проблема объясняется попыткой доступа к элементу по индексу, вы- ходящему за пределы массива. В некоторых языках при этом генерируется ошиб- ка, а в других — получаются причудливые и неожиданные результаты. Обдумайте применение контейнеров вместо массивов или рассматривай- те массивы как последовательные структуры Некоторые именитые в ком- пьютерной науке люди предлагали запретить произвольный доступ к массиву, заменив его последовательным (Mills and Linger, 1986). Аргументтруют они это тем, что произвольный доступ к массиву похож на случайные операторы goto в про- грамме: их применение приводит к неаккуратному, подверженному ошибкам коду, в корректности которого сложно быть уверенным. Поэтому вместо массивов пред- лагается использовать множества, стеки и очереди, доступ к элементам которых выполняется последовательно. Проведя небольшой эксперимент, Миллз (Mills) и Линджер (Linger) вы- яснили, что разработанный таким образом проект потребовал исполь- зования меньшего числа переменных и меньшего числа ссылок на эти переменные. То есть проект был относительно эффективнее, что привело к со- зданию более надежного ПО. Рассмотрите вопрос использования контейнерных классов с последовательным доступом — наборов, стеков, очередей и т. п. — как альтернативу прежде, чем выбрать массив. Проверяйте конечные точки массивов Как бывает по- лезно продумать применение конечных точек в операторе цикла, так и вы сможете обнаружить немало ошибок, прове- рив крайние элементы массивов. Задайтесь вопросом, пра- вильно ли выполняется доступ к первому элементу массива или случайно используется элемент перед ним либо после него. А что с последним элементом? Нет ли в коде ошибки потери единицы? И, наконец, спросите себя, пра- вильно ли код обращается к элементам в середине массива. В многомерном массиве убедитесь, что его индексы используются в пра- вильном порядке Очень легко написать Array[ i ][ j ], имея в виду Array[ j ][ i ], так что не жалейте времени для проверки правильного порядка индексов. Попро- буйте использовать более значимые имена, чем i и j, когда их назначение не вполне очевидно. Остерегайтесь пересечения индексов При использовании вложенных цик- лов легко написать Array[ j ], имея в виду Array[ i ]. Перемена мест индексов назы- вается «пересечением индексов» (index cross-talk). Проверьте эту возможность. Опять же, используйте более значимые имена индексов, чем i и j, чтобы ошибки пересечения изначально сложнее было совершить. В языке C для работы с массивами используйте макрос ARRAY_LENGTH() Вы можете добавить гибкости вашей работе с массивами, определив макрос ARRAY_ LENGTH(): Пример определения макроса ARRAY_LENGTH() на языке C #define ARRAY_LENGTH( x ) (sizeof(x)/sizeof(x[0])) Перекрестная ссылка Вопросы применения массивов и циклов имеют много общего. Подроб- нее о циклах см. главу 16. |