Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 12 Основные типы данных 291 носитесь все#таки с подозрением к выражениям, включающим переменные с обоими префиксами. Объявляйте для строк в стиле C длину, равную КОНСТАНТА+1 В C и C++ ошибки завышения на 1 в C#строках — обычное явление, потому что очень легко забыть, что строка длины n требует для хранения n + 1 байт, и не выделить место для нулевого терминатора (байта в конце строки, установленного в 0). Эффектив# ный способ решения этой проблемы — использовать именованные константы при объявлении всех строк. Суть в том, что именованные константы применяются всегда одинаково: Сначала длина строки объявляется как КОНСТАНТА+1, а затем КОНСТАН% ТА используется для обозначения длины строки во всем остальном коде. Вот пример: Пример правильных объявлений строк (С) /* Объявляем строку длиной «константа+1». Во всех остальных местах программы используем «константа», а не «константа +1». */ Эта строка объявлена с длиной NAME_LENGTH +1. char name[ NAME_LENGTH + 1 ] = { 0 }; /* Длина строки — NAME_LENGTH */ /* Пример 1: Заполняем строку символами ‘A’, используя константу NAME_LENGTH для определения количества символов ‘A’, которые можно скопировать. Заметьте: используется NAME_LENGTH, а не NAME_LENGTH + 1. */ В действиях со строкой NAME_LENGTH используется здесь… for ( i = 0; i < NAME_LENGTH; i++ ) name[ i ] = ‘A’; /* Пример 2: Копируем другую строку в первую, используя константу для определения максимальной длины, которую можно копировать. */ …и здесь. strncpy( name, some_other_name, NAME_LENGTH ); Если у вас не будет соглашения по этому поводу, иногда вы будете объявлять строки длиной NAME_LENGTH, а в операциях использовать NAME_ LENGTH%1; а иногда вы будете объявлять строки длиной NAME_LENGTH+1 и работать с NAME_LENGTH. Каждый раз при использовании строки вам придется вспоминать, как вы ее объявили. Если же вы всегда одинаково объявляете строки, думать, как работать с каждой из них, не надо, и вы избежите ошибок из#за того, что забыли особенность объявле# ния данной строки. Выработка соглашения минимизирует умственную перегруз# ку и ошибки при программировании. Инициализируйте строки нулем во избежание строк бесконечной длины Язык C определяет конец строки пу# тем поиска нулевого терминатора — байта в конце строки, установленного в 0. Какой предполагалась длина строки, зна# > > > Перекрестная ссылка Подроб- нее об инициализации данных см. раздел 10.3. 292 ЧАСТЬ III Переменные чения не имеет: C никогда не найдет ее конец, если не найдет нулевой байт. Если вы забыли поместить нулевой байт в конец строки, строковые операции могут ра# ботать не так, как вы ожидаете. Вы можете предупредить появление бесконечных строк двумя способами. Во#пер# вых, при объявлении инициализируйте символьные массивы 0: Пример правильного объявления символьного массива (C) char EventName[ MAX_NAME_LENGTH + 1 ] = { 0 }; Во#вторых, при динамическом создании строк инициализируйте их 0, используя функцию calloc() вместо malloc(). Функция calloc() выделяет память и инициали# зирует ее 0. malloc() выделяет память без инициализации, поэтому вы рискуете, используя память, выделенную с помощью malloc(). Используйте в C массивы символов вместо указате' лей Если объем занимаемой памяти некритичен (а часто так и есть), объявляйте все строковые переменные как мас# сивы символов. Это поможет избежать проблем с указателями, а компилятор бу# дет выдавать больше предупреждений в случае неправильных действий. Используйте strncpy() вместо strcpy() во избежание строк бесконечной длины Строковые функции в C существуют в опасной и безопасной версиях. Более опасные функции, такие как strcpy() и strcmp(), продолжают работу до обнаруже# ния нулевого терминатора. Их более безобидные спутники — strncpy() и strncmp() — принимают максимальную длину в качестве параметра, так что, даже если строки будут бесконечными, ваши вызовы функций не зациклятся. 12.5. Логические переменные Логические или булевы переменные сложно использовать неправильно, а их вдум# чивое применение сделает вашу программу аккуратней. Используйте логические переменные для документи' рования программы Вместо простой проверки логиче# ского выражения вы можете присвоить его значение пере# менной, которая сделает смысл теста очевидным. Например, в этом фрагменте из условия if не ясно, выполняется ли проверка завершения, ошибочной ситуации или чего#то еще: Пример логического условия, чье назначение неочевидно (Java) if ( ( elementIndex < 0 ) || ( MAX_ELEMENTS < elementIndex ) || ( elementIndex == lastElementIndex ) ) { } В следующем фрагменте применение логических переменных делает назначение if#проверки яснее: Перекрестная ссылка О масси- вах см. раздел 12.8. Перекрестная ссылка Об ис- пользовании комментариев для документирования программы см. главу 32. Перекрестная ссылка Пример использования логической фун- кции для документирования программы см. в подразделе «Упрощение сложных выраже- ний» раздела 19.1. ГЛАВА 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). > |