Главная страница

Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по


Скачать 5.88 Mb.
НазваниеРуководство по стилю программирования и конструированию по
АнкорСовершенный код
Дата31.03.2023
Размер5.88 Mb.
Формат файлаpdf
Имя файлаСовершенный код. Мастер-класс. Стив Макконнелл.pdf
ТипРуководство
#1028502
страница38 из 106
1   ...   34   35   36   37   38   39   40   41   ...   106
ГЛАВА 12 Основные типы данных
283
12.1. Числа в общем
Далее дано несколько рекомендаций, позволяющих сократить число ошибок при использовании чисел.
Избегайте «магических чисел» Магические числа — это обычные числа, такие как
100 или 47524, которые появля#
ются в программе без объяснений. Если вы программируе#
те на языке, поддерживающем именованные константы, ис#
пользуйте их вместо магических чисел. Если вы не можете применить именованные константы, применяйте глобальные переменные, когда это возможно.
Исключение магических чисел дает три преимущества.

Изменения можно сделать более надежно. Если вы используете именованные константы, нет нужды искать каждое из чисел
100, и вы не измените по ошиб#
ке те из этих чисел, что ссылаются на что#либо иное.

Изменения сделать проще. Когда максимальное число элементов меняется со
100 на 200, используя магические числа, вы должны найти каждое число 100
и изменить его на
200. Если вы используете 100+1 или 100%1, вы также долж#
ны найти и изменить все числа
101 и 99 на 201 и 199 соответственно. При использовании именованных констант вы просто меняете определение кон#
станты со
100 на 200 в одном месте.

Ваша программа лучше читается. Конечно, в выражении:
for i = 0 to 99 do ...
можно предположить, что
99 определяет максимальное число элементов. А вот выражение:
for i = 0 to MAX_ENTRIES1 do ...
не оставляет на этот счет сомнений. Даже если вы уверены, что это число ни#
когда не изменится, применяя именованные константы, вы получите более читабельную программу.
Применяйте жестко заданные нули и единицы по необходимости Зна#
чения
0 и 1 используются для инкремента, декремента, а также в начале циклов при нумерации первого элемента массива.
0 в конструкции:
for i = 0 to CONSTANT do ...
вполне приемлем, так же как
1 в выражении:
total = total + 1
Вот хорошее правило: используйте в программе как константы только
0 и 1, а любые другие числа определите как литералы с понятными именами.
Ошибки деления на ноль Каждый раз, когда вы пользуетесь символом деления
(
/ в большинстве языков), думайте о том, может ли в знаменателе оказаться 0. Если такая возможность существует, напишите код, предупреждающий появление ошиб#
ки деления на 0.
Перекрестная ссылка О приме- нении именованных констант вместо магических чисел см.
раздел 12.7.

284
ЧАСТЬ III Переменные
Выполняйте преобразования типов понятно Убедитесь, что кто#нибудь, чи#
тая вашу программу, поймет преобразования между разными типами данных,
которые в ней встречаются. На языке C++ вы могли бы написать:
y = x + (float) i а на Microsoft Visual Basic:
y = x + CSng( i )
Эта практика поможет обеспечить однозначность ваших преобразований — раз#
ные компиляторы по#разному конвертируют, а при таком подходе вы гарантиро#
ванно получите то, что ожидали.
Избегайте сравнений разных типов Если x — число с плавающей запятой, а
i — целое, проверка:
if ( i = x ) then ...
почти гарантированно не сработает. К тому времени, когда компилятор опреде#
лит каждый тип, который он хочет задействовать для сравнения, преобразует один из типов в другой, произведет ряд округлений и вычислит ответ, вы будете рады,
если ваша программа вообще работает. Сделайте преобразования вручную, так чтобы компилятор мог сравнить два числа одного и того же типа, и точно знай#
те, что нужно сравнивать.
Обращайте внимание на предупреждения вашего компилято'
ра Многие современные компиляторы сообщают о наличии разных типов чисел в одном выражении. Обращайте на это внимание! Каждый программист рано или поздно просит кого#нибудь помочь выследить надоедли#
вую ошибку, а выясняется, что о ней все время предупреждал компилятор. Про#
фессионалы высокого класса пишут свои программы так, чтобы исключить все предупреждения компилятора. Легче предоставить работу компилятору, чем вы#
полнять ее самому.
12.2. Целые числа
Учитывайте следующие рекомендации при применении целых чисел.
Проверяйте целочисленность операций деления Когда используются целые числа, выражение 7/10 не равно 0,7. Оно обычно равно 0 или минус бесконечно#
сти, или ближайшему целому, или… ну, вы понимаете. Результат зависит от выб#
ранного языка. Это же относится и к промежуточным результатам. В реальном мире
10 * (7/10) = (10*7) / 10 = 7. Но не в мире целочисленной арифметики. 10 * (7/10)
равно 0, потому что целочисленное деление (7/10) равно 0. Простейший способ исправить положение — преобразовать его так, чтобы операции деления выпол#
нялись последними: (10*7) / 10.
Проверяйте переполнение целых чисел При выполнении умножения или сло#
жения необходимо принимать во внимание наибольшие возможные значения це#
лых чисел. Для целого числа без знака это обычно 2 32
–1, а иногда и 2 16
–1, или 65 535.
Проблема возникает, когда вы умножаете два числа, в результате чего получается
Перекрестная ссылка Вариант этого примера см. в разделе 12.3.

ГЛАВА 12 Основные типы данных
285
число большее, чем максимально возможное целое. Скажем, если вы умножаете
250 * 300, правильным ответом будет 75 000. Но если максимальное целое — 65 535,
то, возможно, из#за переполнения вы получите 9464 (75 000 – 65 536 = 9464). Вот интервалы значений для часто встречающихся целых типов (табл. 12#1):
Табл. 12-1. Интервалы значений некоторых целых типов
Целый тип
Интервал
8#битный со знаком
От –128 до 127 8#битный без знака
От 0 до 255 16#битный со знаком
От –32 768 до 32 767 16#битный без знака
От 0 до 65 535 32#битный со знаком
От –2 147 483,648 до 2 147 483 647 32#битный без знака
От 0 до 4 294 967 295 64#битный со знаком
От –9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 64#битный без знака
От 0 до 18 446 744 073 709 551 615
Простейший способ предотвращения целочисленного переполнения — просмотр каждого члена арифметического выражения с целью представить наибольшее возможное значение, которое он может принимать. Так, если в целочисленном выражении
m = j * k, наибольшим значением для j будет 200, а для k — 25, то мак#
симальным значением для
m будет 200 * 25 = 5 000. Для 32#разрядных машин это вполне допустимо, так как максимальным целым является 2 147 483 647. С другой стороны, если максимально возможное значение для
j — это 200 000, а для k
100 000, то значение
m может достигать 200 000 * 100 000 = 20 000 000 000. Это уже неприемлемо, так как 20 000 000 000 больше, чем 2 147 483 647. В этом слу#
чае для размещения наибольшего возможного значения
m вам придется исполь#
зовать 64#битные целые или числа с плавающей запятой.
Кроме того, учитывайте будущее развитие программы. Если
m никогда не будет больше 5 000 — отлично. Но если ожидается, что
m будет постоянно расти на протяжении нескольких лет, примите это во внимание.
Проверяйте на переполнение промежуточные результаты Число, полу#
чаемое в конце вычислений, — не единственное, о котором следует беспокоить#
ся. Представьте, что у вас есть такой код:
Пример переполнения промежуточных результатов (Java)
int termA = 1000000;
int termB = 1000000;
int product = termA * termB / 1000000;
System.out.println( “( “ + termA + “ * “ + termB + “ ) / 1000000 = “ + product );
Вы можете подумать, что значение
Product вычисляется как (100 000*100 000) /
100 000 и поэтому равно 100 000. Но программе приходится вычислять проме#
жуточное значение
100 000*100 000 до того, как будет выполнено деление на
100 000, а это значит, что нужно хранить такое большое число, как
1 000 000 000 000. Угадайте, что получится? Вот результат:
( 1000000 * 1000000 ) / 1000000 = 727

286
ЧАСТЬ III Переменные
Если значение целых чисел в вашей системе не превышает 2 147 483 647, проме#
жуточный результат слишком велик для целого типа данных. В такой ситуации промежуточный результат, который должен быть равен
1 000 000 000 000, на са#
мом деле равен
727 379 968, поэтому, когда вы делите его на 100 000, вы получа#
ете #
727 вместо 100 000.
Вы можете решить проблему переполнения промежуточных результатов так же,
как и в случае целочисленного переполнения: изменив тип на длинное целое или число с плавающей запятой.
12.3. Числа с плавающей запятой
Главная особенность применения чисел с плавающей запятой в том, что многие дробные десятичные числа не могут быть точно представлены с помощью нулей и единиц, используемых в цифровом компьютере.
В бесконечных десятичных дробях, таких как 1/3 или 1/7, обычно сохраняется только 7 или 15 цифр после запятой. В моей версии Microsoft Visual Basic 32#бит#
ное представление дроби 1/3 в виде числа с плавающей запятой равно 0,33333330.
То есть точность ограничена 7 цифрами. Такая точность достаточна для большин#
ства случаев, но все же способна иногда вводить в заблуждение.
Вот несколько рекомендаций по использованию чисел с плавающей запятой.
Избегайте сложения и вычитания слишком разных по
размеру чисел Для 32#битной переменной с плавающей запятой сумма 1 000 000,00 + 0,1, вероятно, будет равна
1 000 000,00, так как в 32 битах недостаточно значимых цифр, чтобы охватить интервал между 1 000 000 и 0,1. Ана#
логично 5 000 000,02 – 5 000 000,01, вероятно, равно 0,0.
Решение? Если вам нужно складывать настолько разные по величине числа, сна#
чала отсортируйте их, а затем складывайте, начиная с самых маленьких значений.
Аналогично, если вам надо сложить бесконечный ряд значений, начните с наи#
меньшего члена, т. е. суммируйте члены в обратном порядке. Это не решит про#
блемы округления, но минимизирует их. Многие алгоритмические книги предла#
гают решения для таких случаев.
Избегайте сравнений на равенство Числа с плавающей запятой, которые должны быть равны, на самом деле рав#
ны не всегда. Главная проблема в том, что два разных спо#
соба получить одно и то же число не всегда приводят к одинаковому результату. Так, если 10 раз сложить 0,1, то 1,0 получается только в редких случаях. Следующий пример содержит две переменных (
nominal и sum),
которые должны быть равны, но это не так.
Пример неправильного сравнения чисел с плавающей точкой (Java)
Переменная nominal — 64-битное вещественное число.
double nominal = 1.0;
double sum = 0.0;
Перекрестная ссылка Книги,
содержащие алгоритмы реше- ния этих проблем, см. в подраз- деле «Дополнительные ресур- сы» раздела 10.1.
1 равен 2 для достаточно боль- ших значений 1.
Аноним
>

ГЛАВА 12 Основные типы данных
287
for ( int i = 0; i < 10; i++ ) {
sum вычисляется как 10*0,1. Она должна быть равна 1,0.
sum += 0.1;
}
Здесь неправильное сравнение.
if ( nominal == sum ) {
System.out.println( “Numbers are the same.” );
}
else {
System.out.println( “Numbers are different.” );
}
Как вы, наверное, догадались, программа выводит:
Numbers are different.
Вывод каждого значения
sum в цикле for выглядит так:
0.1 0.2 0.30000000000000004 0.4 0.5 0.6 0.7 0.7999999999999999 0.8999999999999999 0.9999999999999999
Таким образом, хорошей идеей будет найти альтернативу операции сравнения на равенство для чисел с плавающей запятой. Один эффективный подход состоит в том, чтобы определить приемлемый интервал точности, а затем использовать логические функции для выяснения, достаточно ли близки сравниваемые значе#
ния. Для этого обычно пишется функция
Equals(), которая возвращает true, если значения попадают в этот интервал, и
false — в противном случае. На языке Java такая функция может выглядеть так:
Пример метода для сравнения чисел с плавающей запятой (Java)
final double ACCEPTABLE_DELTA = 0.00001;
boolean Equals( double Term1, double Term2 ) {
if ( Math.abs( Term1  Term2 ) < ACCEPTABLE_DELTA ) {
return true;
}
else {
return false;
}
}
Перекрестная ссылка Этот при- мер — доказательство того, что из каждого правила есть исклю- чения. Переменные здесь содер- жат цифры в именах. Правило против использования цифр в именах переменных см. в раз- деле 11.7.
>
>

288
ЧАСТЬ III Переменные
Если код в примере «неправильного сравнения чисел с плавающей запятой» из#
менить так, чтобы для сравнения использовался этот метод, новое выражение получит следующий вид:
if ( Equals( Nominal, Sum ) ) ...
При запуске теста программа выведет сообщение:
Numbers are the same.
В зависимости от требований вашего приложения использование жестко закодиро#
ванного значения
ACCEPTABLE_DELTA может быть недопустимо. Возможно, придет#
ся вычислять
ACCEPTABLE_DELTA на основании размера двух сравниваемых чисел.
Предупреждайте ошибки округления Проблемы с ошибками округления сход#
ны с проблемами слишком разных по размеру чисел. У них одинаковые причи#
ны и похожие методики решения. Кроме того, далее перечислены способы реше#
ния проблем округления.

Измените тип переменной на тип с большей точностью. Если вы используете числа с одинарной точностью, замените их числами с двойной точностью и т. д.

Используйте двоично#десятичные переменные (binary coded decimal, BCD). BCD#числа обычно работают медлен#
нее и требуют больше памяти для хранения, но предотвра#
щают множество ошибок округления. Это особенно важно,
если используемые переменные представляют собой долла#
ры и центы или другие величины, которые должны точно балансироваться.

Измените тип с плавающей запятой на целые значения. Это такая самодельная замена BCD#переменных. Возможно, вам придется использовать 64#битные це#
лые, чтобы получить нужную точность. Этот способ предполагает, что вы сами будете отслеживать дробные части чисел. Допустим, изначально вы вели учет денежных сумм, применяя числа с плавающей запятой, при этом центы указы#
вались как дробная часть. Это обычный способ обработки долларов и центов.
Когда вы переключаетесь на целые числа, вам нужно вести учет центов с помо#
щью целых, а долларов — с помощью чисел, кратных 100 центам. Иначе говоря,
вы умножаете сумму в долларах на 100 и храните центы в этой переменной в интервале от 0 до 99. Такое решение может показаться абсурдным, но оно эф#
фективно и с точки зрения скорости, и с точки зрения точности. Вы можете упростить эти манипуляции, создав класс
DollarsAndCents, скрывающий целое представление чисел и предоставляющий необходимые числовые операции.
Проверяйте поддержку специальных типов данных в языке и дополнитель'
ных библиотеках Некоторые языки, включая Visual Basic, предоставляют такие типы данных, как
Currency, предназначенные для данных, чувствительных к ошиб#
кам округления. Если ваш язык содержит встроенный тип данных, предоставляю#
щий такую функциональность, используйте его!
Перекрестная ссылка Как пра- вило, конвертация в тип BCD
минимально влияет на произво- дительность. Если вы озабоче- ны проблемой производитель- ности, см. раздел 25.6.

ГЛАВА 12 Основные типы данных
289
12.4. Символы и строки
Этот раздел предлагает несколько советов по использованию строк. Первый от#
носится к строкам во всех языках.
Избегайте магических символов и строк Магические символы — это литеральные символы (например, '
А'), а ма#
гические строки — это литеральные строки (например,
”Gigamatic Accounting Program”), которые разбросаны по всей программе. Если ваш язык программирования поддерживает применение именованных констант, то лучше задействуй#
те их. В противном случае используйте глобальные переменные. Далее перечис#
лено несколько причин, по которым надо избегать литеральных строк.

Для таких часто встречающихся строк, как имя программы, названия команд,
заголовки отчетов и т. п., вам может понадобиться поменять содержимое. На#
пример,
”Gigamatic Accounting Program” в более поздней версии может изме#
ниться на
”New and Improved! Gigamatic Accounting Program”.

Все большее значение приобретают международные рынки, и строки, сгруп#
пированные в файле ресурсов переводить гораздо легче, чем раскиданные по всей программе.

Строковые литералы обычно занимают много места. Они используются для меню, сообщений, экранов помощи, форм ввода и т. д. Если их слишком мно#
го, они выходят из#под контроля и вызывают проблемы с памятью. Во многих системах объем памяти, занимаемый строками, не является причиной для бес#
покойства. Однако при программировании встроенных систем и других при#
ложений, в которых каждый байт на счету, проблему хранения строк легче решить, если эти строки относительно независимы от кода.

Символьные и строковые литералы могут быть загадочными. Комментарии или именованные константы проясняют ваши намерения. В следующем примере смысл
0x1B неясен. Константа ESCAPE делает значение более понятным.
Пример сравнений с использованием строк (C++)
Плохо!
if ( input_char == 0x1B ) ...
Лучше!
if ( input_char == ESCAPE ) ...
Следите за ошибками завышения/занижения на единицу Поскольку подстро#
ки могут индексироваться аналогично массивам, не забывайте об ошибках завы#
шения/занижения на 1, которые приводят к чтению или записи за концом строки.
Узнайте, как ваш язык и система поддерживают Uni'
code В некоторых языках, например в Java, все строки хра#
нятся в формате Unicode. В других — таких, как C и C++ —
работа со строками в Unicode требует применения отдельного набора функций. Пре#
образование между Unicode и другими наборами символов часто необходимо для взаимодействия со стандартными библиотеками и библиотеками сторонних про#
Перекрестная ссылка Вопросы использования магических сим- волов и строк аналогичны во- просам применения магических чисел (см. раздел 12.1).
http://cc2e.com/1285
>
>

290
ЧАСТЬ III Переменные изводителей. Если часть строк не будет поддерживать Unicode (скажем, в C или C++),
как можно раньше решите, стоит ли вообще использовать символы Unicode. Если вы решились на это, подумайте, где и когда будете это делать.
Разработайте стратегию интернационализации/локализации в ранний
период жизни программы Вопросы, связанные с интернационализацией, от#
носятся к разряду ключевых. Решите, будут ли все строки храниться во внешних ресурсах и будет ли создаваться отдельный вариант программы для каждого язы#
ка или конкретный язык будет определяться во время выполнения.
Если вам известно, что нужно поддерживать толь'
ко один алфавит, рассмотрите вариант использова'
ния набора символов ISO 8859 Для приложений, исполь#
зующих только один алфавит (например, английский), которым не надо поддер#
живать несколько языков или какой#либо идеографический язык (такой как пись#
менный китайский), расширенный ASCII#набор стандарта ISO 8859 — хорошая альтернатива символам Unicode.
Если вам необходимо поддерживать несколько языков, используйте Uni'
code Unicode обеспечивает более полную поддержку международных наборов символов, чем ISO 8859 или другие стандарты.
Выберите целостную стратегию преобразования строковых типов Если вы используете несколько строковых типов, общим подходом, помогающим хра#
нить строковые типы в порядке, будет хранение всех строк программы в одном формате и преобразование их в другой формат как можно ближе к операциям ввода и вывода.
Строки в языке C
Строковый класс в стандартной библиотеке шаблонов C++ решил большинство проблем со строками языка C. А тот, кто напрямую работает с C#строками, ниже узнает о способах избежать часто встречающихся ошибок.
Различайте строковые указатели и символьные массивы Проблемы со строковыми указателями и символьными массивами возникают из#за способа об#
работки строк в C. Учитывайте различия между ними в двух случаях.

Относитесь с недоверием к строковым выражениям, содержащим знак равен#
ства. Строковые операции в C практически всегда выполняются с помощью
strcmp(), strcpy(), strlen() и аналогичных функций. Знаки равенства часто сигна#
лизируют о каких#то ошибках в указателях. Присваивание в C не копирует стро#
ковые константы в строковые переменные. Допустим, у нас есть выражение:
StringPtr = “Some Text String”;
В этом случае
”Some Text String” — указатель на литеральную текстовую строку,
и это присваивание просто присвоит указателю
StringPtr адрес данной стро#
ки. Операция присваивания не копирует содержимое в
StringPtr.

Используйте соглашения по именованию, чтобы различать переменные —
массивы символов и указатели на строки. Одно из общепринятых соглашений —
использование
ps как префикса для обозначения указателя на строку, и ach
как префикса для символьного массива. И хотя они не всегда ошибочны, от#
http://cc2e.com/1292

1   ...   34   35   36   37   38   39   40   41   ...   106


написать администратору сайта