Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 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. ГЛАВА 12 Основные типы данных 309 Перечислимые типы Используются ли в программе перечислимые типы вместо именованных констант ради их улучшенной читабельности, надежности и модифицируе- мости? Используются ли перечислимые типы вместо логических переменных, если все значения переменной не могут быть переданы с помощью true и false? Проверяются ли некорректные значения перечислимых типов в условных операторах? Зарезервирован ли первый элемент перечислимого типа как недопустимый? Перечислимые константы Используются ли в программе именованные константы вместо магических чисел для объявления данных и границ циклов? Используются ли именованные константы последовательно, чтобы одно значение не представлялось в одном месте константой, а в другом — лите- ралом? Массивы Находятся ли все индексы массива в его границах? Свободны ли ссылки на массив от ошибок потери единицы? Указаны ли все индексы многомерных массивов в правильном порядке? В правильном ли порядке используются переменные-индексы во вложенных циклах, не происходит ли пересечения индексов? Создание типов Используются ли в программе отдельные типы для каждого вида данных, который может измениться? Ориентируются ли имена типов на реальные сущности, которые эти типы представляют, а не на типы языка программирования? Достаточно ли наглядны имена типов, чтобы помочь документированию объявлений данных? Не произошло ли переопределение предопределенных типов? Рассматривался ли вопрос создания нового класса вместо простого пере- определения типа? Ключевые моменты 쐽 Работа с определенными типами данных требует запоминания множества правил для каждого из них. Используйте список контрольных вопросов из этой главы, чтобы убедиться, что вы учли основные проблемы с ними. 쐽 Создание собственных типов, если ваш язык это позволяет, упрощает модифи- кацию вашей программы и делает ее более самодокументируемой. 쐽 Прежде чем создавать простой тип с помощью typedef или его эквивалента, подумайте, не следует ли создать вместо него новый класс. 310 ЧАСТЬ III Переменные Г Л А В А 1 3 Нестандартные типы данных Содержание 쐽 13.1. Структуры 쐽 13.2. Указатели 쐽 13.3. Глобальные данные Связанные темы 쐽 Фундаментальные типы данных: глава 12 쐽 Защитное программирование: глава 8 쐽 Нестандартные управляющие структуры: глава 17 쐽 Сложность в разработке ПО: раздел 5.2 Некоторые языки программирования поддерживают экзотические виды данных в дополнение к типам, обсуждавшимся в главе 12. В разделе 13.1 рассказывается, при каких обстоятельствах вы могли бы использовать структуры вместо классов. В разделе 13.2 описываются детали использования указателей. Если у вас возни- кали проблемы с использованием глобальных данных, из раздела 13.3 вы узнае- те, как их избежать. Если вы думаете, что типы данных, описанные в этой главе, — это не те типы, о которых вы обычно читаете в современных книгах по объек- тно-ориентированному программированию, то вы абсолютно правы. Поэтому эта глава и называется « Нестандартные типы данных». 13.1. Структуры Термин «структура» относится к типу данных, построенному на основе других типов. Так как массивы — особый случай, они рассматриваются отдельно в главе 12. В этом разделе обсуждаются структурированные данные, созданные пользо- вателем: structs в C/C++ и Structures в Microsoft Visual Basic. В Java и C++ классы тоже иногда выглядят, как структуры (когда они состоят только из открытых членов данных и не содержат открытые методы). Чаще всего вы предпочтете создавать классы, а не структуры, чтобы задейство- вать преимущества закрытости и функциональности, предлагаемой классами, в http://cc2e.com/1378 |