Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
книги хакеры / Питер_Гудлиф_Ремесло_программиста_Практика_написания_хорошего_кода.pdf
Скачиваний:
15
Добавлен:
19.04.2024
Размер:
9.23 Mб
Скачать

 

 

 

 

hang

e

 

 

 

 

 

 

C

 

E

 

 

 

X

 

 

 

 

 

-

 

 

 

 

 

d

 

F

 

 

 

 

 

 

t

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

to

 

 

 

 

w Click

 

 

 

Ограниченияm

 

 

 

 

w

 

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

.

 

 

 

 

 

.c

 

 

p

 

 

 

 

g

 

 

 

 

df

 

 

n

e

 

 

 

 

-xcha

 

 

 

 

 

 

 

hang

e

 

 

 

 

 

 

 

C

 

E

 

 

 

 

X

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

F

 

 

 

 

 

 

t

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

 

to

 

 

 

 

 

45Click

 

 

 

 

 

m

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

 

w

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

.c

 

 

.

 

 

 

 

 

 

 

 

p

 

 

 

 

g

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

-x cha

 

 

 

 

Пользуйтесь идиомами языка

Следуя этому простому совету, вы гарантируете, что ваш код будет понятен читающим его. Они реже будут ошибаться.

Проверяйте числовые результаты

Даже самые простые вычисления могут приводить к переполнению или потере точности. Будьте настороже. Спецификации языков и ба% зовые библиотеки предоставляют механизмы для определения до% пустимых значений стандартных типов данных – воспользуйтесь ими. Вы должны знать, какие числовые типы существуют и для ка% ких целей их лучше применять.

Проверяйте допустимость каждого вычисления. Например, следи% те за тем, чтобы ваши величины не приводили к делению на ноль.

Соблюдайте защищенность констант

Особенно это облегчает жизнь при программировании на C/C++. Ста% райтесь объявлять как const все, что только можно. Этим достигают% ся две цели: квалификаторы const способствуют документированию кода и позволяют компилятору обнаруживать глупые ошибки. Он не позволит вам изменить данные, модификация которых запрещена.

Ограничения

Как уже говорилось, при составлении программы мы делаем ряд неяв% ных предположений. Но как физически учесть эти предположения

вкоде, чтобы они не привели к возникновению неуловимых проблем? Нужно просто написать некоторый дополнительный код для проверки всех этих условий. Такой код выступает в роли документации каждого предположения, переводя его в явный вид.1 В результате мы приводим

всистему ограничения, налагаемые на функциональность и поведение программы.

Каких действий хотим мы от программы в случае нарушения ограниче% ний? Эти ограничения не укладываются в рамки простых обнаружимых и исправимых ошибок времени исполнения (их обработка уже должна присутствовать в коде) и представляют собой, по%видимому, ошибку в логике программы. Есть несколько вариантов реакции программы:

Сделать вид, что ничего не случилось, и надеяться на лучшее.

Оштрафовать на месте и разрешить дальнейшее движение (напри% мер, напечатать диагностическое предупреждение или записать его в журнал).

Сразу арестовать; запретить двигаться дальше (например, прервать выполнение программы контролируемым или неконтролируемым образом).

1Это не значит, что не нужно писать хорошую документацию.

 

 

 

 

hang

e

 

 

 

 

 

 

C

 

E

 

 

 

X

 

 

 

 

 

-

 

 

 

 

 

d

 

F

 

 

 

 

 

 

t

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

to

 

 

 

 

w Click

 

 

 

46m

 

 

 

 

w

 

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

.

 

 

 

 

 

.c

 

 

p

 

 

 

 

g

 

 

 

 

df

 

 

n

e

 

 

 

 

-xcha

 

 

 

 

 

 

 

hang

e

 

 

 

 

 

 

 

C

 

E

 

 

 

 

X

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

F

 

 

 

 

 

 

t

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

 

to

 

 

 

 

 

Глава 1. Держим оборонуClick

 

 

 

 

 

m

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

 

w

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

.c

 

 

.

 

 

 

 

 

 

 

 

p

 

 

 

 

g

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

-x cha

 

 

 

 

Например, в C недопустимо вызывать функцию strlen с указателем на строку, равным нулю, поскольку указатель будет сразу разыменован. В этом случае подходят последние два варианта. Вероятно, лучше все% го сразу прервать выполнение программы, потому что в незащищен% ных операционных системах разыменование нулевого указателя мо% жет привести к катастрофическим последствиям.

Существует несколько разных сценариев использования ограничений:

Входные условия

Эти условия должны быть выполнены до входа в раздел кода. Если входное условие не выполнено, это означает, что в коде клиента есть ошибка.

Выходные условия

Эти условия должны быть выполнены после выхода из блока кода. Если выходное условие не выполнено, это означает, что в коде по% ставщика есть ошибка.

Инварианты

Эти условия должны быть выполнены при достижении в ходе вы% полнения программы определенной точки, например между прохо% дами цикла, при вызове методов и т. п. Невыполнение инварианта означает, что в логике программы есть ошибка.

Операторы контроля

Это любое другое утверждение относительно состояния программы в данный момент.

Первые два из перечисленных сценариев трудно реализовать при от% сутствии необходимой поддержки в языке – если у функции несколь% ко точек выхода,1 то проверка условий выхода становится хлопотной. Eiffel поддерживает входные и выходные условия в базовом языке и может также гарантировать отсутствие побочных эффектов прове% рок ограничений.

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

Какие ограничения налагать

Есть несколько проблем, которые можно решать с помощью ограниче% ний. Например, можно делать следующее:

Проверять, чтобы все обращения к массивам не выходили за их гра% ницы.

1Спор о том, можно ли разрешать функции иметь несколько точек выхода,

является религиозным.

 

 

 

 

hang

e

 

 

 

 

 

 

C

 

E

 

 

 

X

 

 

 

 

 

-

 

 

 

 

 

d

 

F

 

 

 

 

 

 

t

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

to

 

 

 

 

w Click

 

 

 

Ограниченияm

 

 

 

 

w

 

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

.

 

 

 

 

 

.c

 

 

p

 

 

 

 

g

 

 

 

 

df

 

 

n

e

 

 

 

 

-xcha

 

 

 

 

 

 

 

hang

e

 

 

 

 

 

 

 

C

 

E

 

 

 

 

X

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

F

 

 

 

 

 

 

t

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

 

to

 

 

 

 

 

47Click

 

 

 

 

 

m

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

 

w

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

.c

 

 

.

 

 

 

 

 

 

 

 

p

 

 

 

 

g

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

-x cha

 

 

 

 

Контролировать неравенство нулю указателей перед их разымено% ванием.

Проверять допустимость параметров функций.

Контролировать результаты функций, прежде чем их возвращать.

Проверять состояние объекта перед операциями с ним.

Защищать те участки кода, где должны быть комментарии. Они не должны получать управление.

Первые два случая особенно актуальны для C/C++. В Java, C# и неко% торых других языках есть встроенные средства, помогающие избе% жать эти ловушки.

Каким должен быть объем проверок ограничений? Ставить проверки на каждой второй строке было бы излишеством. Как и в других случа% ях, чувство меры приходит с опытом. Что лучше – перебрать или недо% брать? Избыток проверок может затуманить логику программы. «Чи% таемость – лучший из критериев качества программы: если ее легко чи% тать, то, скорее всего, это хорошая программа; если ее трудно читать, то, скорее всего, это нехорошая программа». (Kernighan Plaugher 76)

На практике достаточно бывает разместить проверки входных и вы% ходных условий в основных функциях плюс инварианты в основных циклах.

Снятие ограничений

Такого рода проверка ограничений обычно требуется только на этапах разработки и отладки программы. После того как мы воспользовались ограничениями, чтобы убедить себя (возможно, ошибочно) в правиль% ности логики программы, следовало бы убрать их, чтобы готовая про% грамма не делала лишней работы.

Это вполне достижимо благодаря чудесам современных технологий. В стандартных библиотеках C и C++ есть механизм для реализации ограничений – оператор контроля assert. Оператор assert действует как процедурный защитный экран, проверяя логику своего аргумента. Его назначение – уведомить разработчика о том, что программа ведет себя некорректно. При этом он не должен запускать никакого кода, взаимодействующего с клиентом. Если контрольное условие выполне% но, исполнение кода продолжается. В противном случае программа аварийно завершается, генерируя сообщение об ошибке. Например:

bugged.cpp:10: int main(): Assertion "1 == 0" failed.

Оператор assert реализован в виде макроса препроцессора и поэтому больше подходит для C, чем для C++. Существует ряд библиотек опе% раторов контроля, более ориентированных на C++.

Чтобы использовать assert, необходимо подключить #include <assert.h>. Затем можно вставлять в свои функции что%то типа assert(ptr != 0);. Препроцессор позволяет убрать контрольные проверки при окончатель%

 

 

 

 

hang

e

 

 

 

 

 

 

C

 

E

 

 

 

X

 

 

 

 

 

-

 

 

 

 

 

d

 

F

 

 

 

 

 

 

t

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

to

 

 

 

 

w Click

 

 

 

48m

 

 

 

 

w

 

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

.

 

 

 

 

 

.c

 

 

p

 

 

 

 

g

 

 

 

 

df

 

 

n

e

 

 

 

 

-xcha

 

 

 

 

 

 

 

hang

e

 

 

 

 

 

 

 

C

 

E

 

 

 

 

X

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

F

 

 

 

 

 

 

t

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

 

to

 

 

 

 

 

Глава 1. Держим оборонуClick

 

 

 

 

 

m

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

 

w

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

.c

 

 

.

 

 

 

 

 

 

 

 

p

 

 

 

 

g

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

-x cha

 

 

 

 

ной сборке программы путем передачи компилятору флага NDEBUG. В результате все операторы assert будут удалены и их аргументы не будут вычисляться. Это значит, что на эффективность работы оконча% тельной сборки программы операторы контроля не будут оказывать никакого влияния.

Следует ли убрать проверки вообще или просто деактивировать их – вопрос спорный. Одни считают, что если их удалить, то в тестирова% нии будет участвовать уже совсем другой код.1 Другие говорят, что на% личие в окончательной версии продукта операторов контроля непри% емлемо и их нужно полностью удалить. (Но кто проводил профилиро% вание программ, чтобы доказать это?)

Во всяком случае, операторы контроля не должны иметь побочных эф% фектов. Что будет, например, если вы по ошибке напишете:

int i = взятьНекоеЧисло();

assert(i = 6); // да... аккуратнее нужно быть! printf("i is %d\n", i);

Очевидно, что при отладке оператор контроля никогда не сработает: его значение = 6 (для C это все равно что истина). Однако при оконча% тельной сборке строка assert будет полностью удалена, и printf выве% дет что%то другое. Таким образом могут возникать скрытые проблемы на поздних стадиях разработки. Не так просто защититься от ошибок в коде, проверяющем наличие ошибок!

Нетрудно представить себе ситуации, в которых операторы контроля могут иметь еще более скрытые побочные эффекты. Например, если контроль выглядит как assert(invariants());, где у функции invari ants() есть побочный эффект, то его непросто обнаружить.

Поскольку операторы контроля могут быть удалены из окончательно% го кода, очень важно, чтобы при контроле выполнялась проверка огра% ничений и ничего больше. Действительные проверки состояний ошиб% ки, таких как неудачное выделение памяти или проблемы в файловой системе, должны проводиться в обычном коде. Вы же не собираетесь изъять их из своей программы? Законные ошибки времени исполне% ния (сколь бы нежелательны они ни были) должны обнаруживаться защитным кодом, который нельзя удалить.

В Java реализован аналогичный механизм контроля.2 Он активизирует% ся средствами управления JVM и генерирует исключение (java.lang.As

1На практике окончательная версия и версия разработки могут иметь еще больше отличий, например в отношении уровня оптимизации или включе% ния символов отладки. В этом случае окончательная версия может иметь тонкие отличия, мешающие проявиться другим ошибкам. Даже на самых ранних этапах разработки тестирование нужно проводить в равной мере и для версии разработчика, и для окончательной сборки.

2Он появился в JDK 1.4 и отсутствует в более ранних версиях.

 

 

 

 

hang

e

 

 

 

 

 

 

C

 

E

 

 

 

X

 

 

 

 

 

-

 

 

 

 

 

d

 

F

 

 

 

 

 

 

t

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

to

 

 

 

 

w Click

 

 

 

Ограниченияm

 

 

 

 

w

 

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

.

 

 

 

 

 

.c

 

 

p

 

 

 

 

g

 

 

 

 

df

 

 

n

e

 

 

 

 

-xcha

 

 

 

 

 

 

 

hang

e

 

 

 

 

 

 

 

C

 

E

 

 

 

 

X

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

F

 

 

 

 

 

 

t

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

 

to

 

 

 

 

 

49Click

 

 

 

 

 

m

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

 

w

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

.c

 

 

.

 

 

 

 

 

 

 

 

p

 

 

 

 

g

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

-x cha

 

 

 

 

sertionError), не прерывая сразу выполнение программы. В .NET меха% низм контроля реализован посредством класса Debug.

Если вы обнаружили и исправили ошибку, очень полезно вставить операторы контроля в исправленное место. Это поможет вам не насту% пить на те же грабли вторично. По крайней мере, останется полезное предупреждение для тех, кто будет потом сопровождать ваш код.

В C++/Java есть стандартный прием задания ограничений классов пу% тем добавления в каждый класс одной функции%члена bool invariant(). (Естественно, что эта функция не должна давать побочных эффектов.) После этого в начале и в конце каждой функции%члена можно помес% тить assert, вызывающий этот инвариант. (По очевидным причинам операторов контроля не должно быть в начале конструктора или кон% це деструктора.) Например, инвариант класса circle может проверять условие radius != 0; в противном случае объект находился бы в недо% пустимом состоянии, чреватом ошибками в последующих вычислени% ях (например, вызовом деления на нуль).

Агрессивное программирование?

Лучший способ защиты – нападение.

Поговорка

Работая над этой главой, я подумал: а какой стиль служит про% тивоположностью защитного программирования? Разумеется,

агрессивное программирование!

Я знаю нескольких программистов, которых можно было бы на% звать «агрессивными». Но мне кажется, что дело тут не ограни% чивается руганью перед компьютером и нежеланием пользо% ваться душем.

Разумно предположить, что агрессивному программированию должно быть свойственно активное стремление взломать код, а не воздвигать защиту от возможных проблем. Иными словами, не защищать код, а активно атаковать его. Я бы назвал это тести% рованием. Как будет показано в разделе «Кто, что, когда, зачем?» на стр. 187, правильно организованное тестирование оказывает огромное положительное влияние на производство программно% го обеспечения. Оно заметно повышает качество кода и стабили% зирует процесс разработки.

Всем программистам следовало бы быть агрессивными.