Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 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. ГЛАВА 12 Основные типы данных 303 При выполнении операций над массивами для указания верхней границы исполь# зуйте макрос ARRAY_LENGTH() вместо именованной константы. Например: Пример использования макроса ARRAY_LENGTH() для операций с массивами на языке C ConsistencyRatios[] = { 0.0, 0.0, 0.58, 0.90, 1.12, 1.24, 1.32, 1.41, 1.45, 1.49, 1.51, 1.48, 1.56, 1.57, 1.59 }; Вот здесь используется макрос. for ( ratioIdx = 0; ratioIdx < ARRAY_LENGTH( ConsistencyRatios ); ratioIdx++ ); Этот способ особенно полезен для массивов неопределенного размера, как в этом примере. Если вы добавляете или удаляете элементы, вам не надо помнить об изменении именованной константы, определяющей размер массива. Разумеется, эта технология работает и с массивами заданного размера, но, используя этот подход, вам не всегда надо будет создавать дополнительные именованные кон# станты для объявления массивов. 12.9. Создание собственных типов данных (псевдонимы) Типы данных, определяемые программистом, — одна из наиболее мощ# ных возможностей, позволяющих наиболее четко обозначить ваше понимание программы. Они защищают программу от непредвиденных изменений и упрощают ее прочтение, и все это — без необходимости проекти# ровать, разрабатывать или тестировать новые классы. Если вы программируете на C, C++ или других языках, поддерживающих такие типы, задействуйте это преиму# щество! Чтобы оценить возможности создания типов, представьте, что вы пишете программу для преобразования координат из сис# темы x, y, z в широту, долготу и высоту. Вам кажется, что могут потребоваться числа с плавающей запятой двойной точнос# ти, но пока вы абсолютно в этом не уверены, предпочитаете писать программу, используя числа с одинарной точностью. Вы можете создать но# вый тип данных специально для координат, применив оператор typedef в C или C++ или его эквивалент в другом языке. Вот как вы определите такой тип в C++: Пример создания типа (C++) typedef float Coordinate; // для координатных переменных Это определение объявляет новый тип Coordinate, функционально идентичный типу float. Чтобы задействовать этот тип, вы просто объявляете с ним переменные точ# но так же, как и с любым предопределенным типом вроде float. Пример: Перекрестная ссылка Во многих случаях лучше создавать класс, чем простой тип данных. Под- робнее см. главу 6. > 304 ЧАСТЬ III Переменные Пример использования созданного типа (C++) Routine1( ... ) { Coordinate latitude; // широта в градусах Coordinate longitude; // долгота в градусах Coordinate elevation; // высота в метрах от центра Земли } Routine2( ... ) { Coordinate x; // координата x в метрах Coordinate y; // координата y в метрах Coordinate z; // координата z в метрах } Здесь все переменные latitude, longitude, elevation, x, y и z объявлены с типом Coordinate. Теперь допустим, что программа изменилась и вы выяснили, что все#таки нужны переменные с двойной точностью. Поскольку вы создали тип специально для координатных данных, все, что вам нужно изменить, — это определение типа. И сделать это вам необходимо только в одном месте — в выражении typedef. Вот как выглядит новое определение типа: Пример измененного определения типа (C++) Первоначальный тип float заменен на double. typedef double Coordinate; // для координатных переменных Вот еще один пример — теперь на языке Pascal. Представьте, что вы разрабатыва# ете систему расчета заработной платы, в которой длина имен работников не пре# вышает 30 символов. Пользователи сказали вам, что ни у кого нет имени длинней 30 символов. Закодируете ли вы число 30 по всей программе? Если да, то вы дове# ряете вашим пользователям гораздо больше, чем я — своим. Лучший подход со# стоит в определении типа для имен работников: Пример создания типа для имен работников (Pascal) Type employeeName = array[ 1..30 ] of char; Когда речь идет о строке или массиве, обычно разумно определить именованную константу, содержащую длину строки или массива, а затем задействовать ее в определении типа. Вы найдете в своей программе много мест, в которых стоит использовать константу, и это — первое из них. Вот как это выглядит: Пример лучшего создания типа (Pascal) Const > ГЛАВА 12 Основные типы данных 305 Вот объявление именованной константы. NAME_LENGTH = 30; Type Здесь эта именованная константа используется. employeeName = array[ 1..NAME_LENGTH ] of char; Еще более усовершенствованный пример может комбинировать идею создания собственных типов с технологией сокрытия информации. Порой сведения, ко# торые вы хотите скрыть, и есть информация о типе данных. Пример с координатами на C++ частично удовлетворяет принципу сокрытия ин# формации. Если вы всегда будете использовать Coordinate вместо float или double, вы эффективно спрячете исходный тип данных. В C++ это практически все воз# можное сокрытие информации, которое язык позволяет сделать разработчику. Все последующие пользователи вашего кода должны соблюдать дисциплину и не смот# реть на определение Coordinate. C++ предоставляет скорее фигуральную, а не бук# вальную возможность сокрытия информации. Другие языки, например Ada, делают шаг вперед и поддерживают буквальное со# крытие информации. Вот как фрагмент кода для типа Coordinate будет выглядеть в модуле Ada, где он был объявлен: Пример сокрытия деталей реализации типа внутри модуля (Ada) package Transformation is Это выражение объявляет Coordinate скрытым в данном модуле. type Coordinate is private; Вот как тип Coordinate будет выглядеть в другом модуле, где он используется: Пример использования типа из другого модуля (Ada) with Transformation; procedure Routine1(...) ... latitude: Coordinate; longitude: Coordinate; begin операторы, использующие широту и долготу end Routine1; Заметьте: тип Coordinate объявлен в модуле как private. Это значит, что единственная часть программы, которая знает определение типа Coordinate, — это закрытая часть модуля Transformation. При групповой разработке проекта вы можете распрост# ранить только спецификацию модуля, что затруднит программисту, работающе# му с другим модулем, просмотр исходного типа Coordinate. Информация будет > > > 306 ЧАСТЬ III Переменные буквально спрятана. Такие языки, как C++, которые требуют распространять определение типа Coordinate в заголовочном файле, подрывают идею реального сокрытия информации. Следующие примеры иллюстрируют несколько причин для создания собственных типов. Упростить модификацию кода Сделать новый тип легко, а это дает вам большую гибкость. Избежать излишнего распространения информации Явная типизация распространяет сведения о типе данных по всей программе вместо их цент# рализации в одном месте. Это пример принципа сокрытия информации с це# лью достижения централизации, (см. раздел 6.2). Увеличить надежность В Ada вы можете объявлять типы как type Age is range 0..99. После этого компилятор генерирует проверки времени выполнения, чтобы удостовериться, что значение любой переменной типа Age всегда попадает в диапазон 0..99. Замаскировать недостатки языка Если ваш язык не содержит необходи# мого предопределенного типа, вы можете создать его сами. Например, в C нет булева или логического типа. Этот недостаток легко исправить, создав тип: typedef int Boolean; Почему приведены примеры создания типов на языках Pascal и Ada? Языки Pascal и Ada сейчас подобны динозаврам, а языки, заменившие их, в основном гораздо практичнее. Однако в области определения простых типов мне кажется, что C++, Java и Visual Basic представляют случай трех шагов вперед и одного шага назад. В Ada такое объявление, как: currentTemperature: INTEGER range 0..212; содержит важную семантическую информацию, которую объявление: int temperature; не содержит. Если посмотреть глубже, то определение: type Temperature is range 0..212; currentTemperature: Temperature; позволяет компилятору удостовериться, что currentTemperature присваивается только другим переменным типа Temperature, и такая дополнительная прослойка безопасности требует минимального кодирования. Естественно, программист может создать класс Temperature, чтобы реализовать те же семантические правила, автоматически предоставляемые в Ada, но между со# зданием простого типа данных в одну строку и созданием класса дистанция ог# ромного размера. Зачастую программист будет использовать простой тип данных, но не станет делать дополнительных усилий для создания класса. ГЛАВА 12 Основные типы данных 307 Основные принципы создания собственных типов Имейте в виду следующие принципы, когда решите создавать собственный тип. Создавайте типы с именами, отражающими их функ' циональность Избегайте имен типов, которые ссылаются на данные, лежащие в основе этих типов. Используйте имена, которые отражают те элементы реальной задачи, которые этот тип представляет. Определения из предыдущих приме# ров — понятно названные типы для координат и имен ра# ботников — это реальные сущности. Точно так же вы можете создавать типы для валюты, кодов платежей, возрастов и т. д., а именно для аспектов действительно существующих задач. Будьте осторожны, создавая имена типов, ссылающиеся на предопределенные типы. Такие имена, как BigInteger или LongString, описывают компьютерные данные, а не конкретную задачу. Большое преимущество создания собственных типов данных состоит в том, что добавляется слой, изолирующий программу от языка разработки. Имена типов, ссылающиеся на типы языка, лежащие в их основе, нарушают эту изоляцию. Они не дают вам большого преимущества по сравнению с примене# нием предопределенных типов. Проблемно#ориентированные имена, с другой стороны, облегчают процесс внесения изменений и предоставляют самодокумен# тируемые объявления типов. Избегайте предопределенных типов Если есть хоть малейшая возможность, что тип может измениться, избегайте применения предопределенных типов вез# де, кроме определений typedef или type. Легко создать новые функционально#ори# ентированные типы — менять же данные в программе, использующей жестко за# кодированные типы, гораздо сложней. Более того, функционально#ориентирован# ные типы частично документируют объявленные с ними переменные. Объявле# ние Coordinate x сообщит вам об x гораздо больше, чем объявление float x. Исполь# зуйте собственные типы везде, где только можно. Не переопределяйте предопределенные типы Изменение определения стан# дартного типа может вызвать путаницу. Например, если в вашем языке есть пре# допределенный тип Integer, не создавайте свой тип с именем Integer. Читающие ваш код могут забыть, что вы его переопределили, и будут считать, что видят тот же Integer, который привыкли видеть. Определите подстановки для переносимости В отличие от совета не изме# нять определение стандартных типов вы можете создать для этих типов подста# новки, так что на разных платформах переменные будут представлены одними и теми же сущностями. Так, вы можете определить тип INT32 и использовать его вместо int или тип LONG64 вместо long. Изначально единственной разницей между двумя типами будет применение заглавных букв. Но при переходе на другую плат# форму вы сможете переопределить варианты с большими буквами так, чтобы они совпадали с типами для данных аппаратных средств. Не создавайте типы, которые легко перепутать с предопределенными. Существу# ет возможность определить INT вместо INT32, но лучше сделать явное различие между типами, созданными вами, и типами, предоставленными языком програм# мирования. Перекрестная ссылка В каждом случае следует решать, не луч- ше ли использовать класс, а не простой тип данных. Подробнее см. главу 6. 308 ЧАСТЬ III Переменные Рассмотрите вопрос создания класса вместо использования typedef Прос# тые операторы typedef позволяют проделать большой путь в сторону сокрытия ин# формации об исходном типе переменной. Однако иногда вам может потребоваться дополнительная гибкость и управляемость, которой позволяют добиться классы. Подробнее см. главу 6. Контрольный список: основные данные Числа в общем Не содержит ли код магические числа? Предупреждаются ли в коде ошибки деления на ноль? Очевидны ли преобразования типов? Если переменные двух разных типов используются в од- ном выражении, будет ли оно вычислено так, как вы это предполагаете? Не происходит ли сравнение переменных разных типов? Компилируется ли программа без предупреждений ком- пилятора? Целые числа Работают ли выражения, содержащие целочисленное деление так, как это предполагалось? Предупреждаются ли в целочисленных выражениях проблемы целочислен- ного переполнения? Числа с плавающей запятой Не содержит ли код операции сложения и вычитания слишком разных по величине чисел? Предупреждаются ли в коде ошибки округления? Не выполняется сравнение на равенство чисел с плавающей запятой? Символы и строки Не содержит ли код магических символов и строк? Свободны ли операции со строками от ошибки потери единицы? Различаются ли в коде на C строковые указатели и массивы символов? Соблюдается ли в коде на C соглашение об объявлении строк с длиной CONSTANT+1? Используются ли в C массивы символов вместо указателей там, где это допустимо? Инициализируются ли в C строки с помощью NULL во избежание бесконеч- ных строк? Используются ли в коде на C strncpy() вместо strcpy()? А strncat() и strncmp()? Логические переменные Используются ли в программе дополнительные логические переменные для документирования проверок условия? Используются ли в программе дополнительные логические переменные для упрощения проверок условия? http://cc2e.com/1206 Перекрестная ссылка Список вопросов, затрагивающих дан- ные вообще, без подразделения на конкретные типы, см. в кон- трольном списке главы 10. Спи- сок вопросов по вариантам именования см. в контрольном списке главы 11. |