Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 10 Общие принципы использования переменных 237 Настройте компилятор так, чтобы он автоматически инициализиро- вал все переменные Если ваш компилятор поддерживает такую возможность, заставьте его автоматически инициализировать все переменные. Однако, полагаясь на компилятор, вы можете столкнуться с проблемами при переносе кода на другой компьютер или при использовании другого компилятора. Документируйте исполь- зование параметров компилятора — без такой документации предположения, осно- ванные на конкретных параметрах компилятора, определить очень трудно. Внимательно изучайте предупреждения компилятора Многие компиля- торы предупреждают об использовании неинициализированных переменных. Проверяйте корректность входных параметров Это еще один эффективный способ предотвращения ошибок инициализации. Прежде чем присвоить входные значения чему-либо, убедитесь, что они допустимы. Используйте утилиту проверки доступа к памяти для обнаружения не- верно инициализированных указателей Некоторые ОС сами следят за кор- ректностью обращений к памяти, выполняемых при помощи указателей, другие ос- тавляют вас на произвол судьбы. Тогда можно приобрести инструмент проверки доступа к памяти и проконтролировать использование указателей в своей программе. Инициализируйте рабочую память при запуске программы Инициализа- ция рабочей памяти известным значением облегчает поиск ошибок инициализа- ции. Этого позволяют достичь описанные ниже подходы. 쐽 Вы можете использовать специализированную утилиту для заполнения памя- ти определенным значением перед запуском программы. Для некоторых це- лей хорошо подходит значение 0, потому что оно гарантирует, что неиници- ализированные указатели будут указывать на нижнюю область памяти, благо- даря чему их будет относительно легко найти. В случае процессоров с архи- тектурой Intel целесообразно заполнить память значением 0xCC, потому что оно соответствует машинному коду команды точки прерывания; если вы запу- стите код в отладчике и попытаетесь выполнить данные, а не код, вы потонете в точках прерывания. Еще одно достоинство значения 0xCC в том, что его легко заметить в дампах памяти; кроме того, оно редко используется. По этим же причинам Брайан Керниган и Роб Пайк предлагают заполнять память констан- той 0xDEADBEEF 1 (Kernighan and Pike, 1999). 쐽 Если вы применяете утилиту заполнения памяти, можете время от времени изменять используемое ей значение. «Встряхивание» программы иногда позво- ляет обнаружить проблемы, которые остаются скрытыми, если среда никогда не изменяется. 쐽 Вы можете сделать так, чтобы программа инициализировала свою рабочую память при запуске. Цель заполнения памяти до запуска программы — обна- ружение дефектов, тогда как цель этого подхода — их сокрытие. Заполняя рабочую память каждый раз одинаковым значением, вы сможете гарантиро- вать, что программа не будет зависеть от случайных изменений начальной конфигурации памяти. Перекрестная ссылка О проверке входных параметров см. главу 8, преимущественно раздел 8.1. 1 Букв. «мертвая корова». — Прим. перев. 238 ЧАСТЬ III Переменные 10.4. Область видимости Область видимости можно понимать как «известность» переменной в програм- ме. Областью видимости называют фрагмент программы, в котором переменная известна и может быть использована. Переменная с ограниченной или неболь- шой областью видимости известна только в небольшом фрагменте программы: в качестве примера можно привести индекс, используемый в теле одного неболь- шого цикла. Переменная с большой областью видимости известна во многих местах программы: примером может служить таблица с данными о сотрудниках, исполь- зуемая по всей программе. В разных языках реализованы разные подходы к области видимости. В некото- рых примитивных языках все переменные глобальны. В этом случае вы не имее- те контроля над областью видимости переменных, что создает много проблем. В C++ и похожих языках переменная может иметь область видимости, соответ- ствующую блоку (фрагменту кода, заключенному в фигурные скобки), методу, классу (возможно, и производным от него классам) или всей программе. В Java и C# переменная может также иметь область видимости, соответствующую пакету или пространству имен (набору классов). Ниже я привел ряд советов, относящихся к области видимости. Локализуйте обращения к переменным Код, расположенный между обращениями к переменной, является «окном уязви- мости». Чем больше это окно, тем выше вероятность, что в его пределах будет добавлен новый код, искажающий значение переменной, и тем труднее следить за значением переменной при чтении кода. Поэтому обращения к переменной всегда целесообразно локализовать, группируя их вместе. Идея локализации обращений к переменным самоочевидна, однако она допуска- ет и формальную оценку. Одним из методов оценки степени сгруппированности обращений к переменной является определение «интервала» (span) между обра- щениями, например: Пример определения интервалов между обращениями к переменным (Java) a = 0; b = 0; c = 0; a = b + c; В данном случае между первым и вторым обращениями к a находятся две строки кода, поэтому и интервал равен 2. Между двумя обращениями к b — одна строка, что дает нам интервал, равный 1, ну а интервал между обращениями к c равен 0. Вот еще один пример: ГЛАВА 10 Общие принципы использования переменных 239 Пример интервалов, равных 1 и 0 (Java) a = 0; b = 0; c = 0; b = a + 1; b = b / c; В этом примере между первым и вторым обращениями к b — одна строка кода, а между вторым и третьим обращениями строк нет, поэтому интервалы равны соответственно 1 и 0. Средний интервал вычисляется путем усреднения отдельных интервалов. Так, во втором примере средний интервал между обращениями к b равен (1+0)/2, или 0,5. Локализовав обра- щения к переменным, вы позволите программисту, который будет читать ваш код, сосредоточиваться на меньшем фрагменте программы в каждый конкретный мо- мент времени. Если обращения будут распределены по большему фрагменту кода, уследить за ними будет сложнее. Таким образом, главное преимущество локали- зации обращений к переменным в том, что оно облегчает чтение программы. Делайте время жизни переменных как можно короче С интервалом между обращениями к переменной тесно связано «время жизни» переменной — общее число строк, на протяжении которых переменная исполь- зуется. Жизнь переменной начинается при первом обращении к ней, а заканчи- вается при последнем. В отличие от интервала время жизни переменной не зависит от числа обраще- ний к ней между первым и последним обращениями. Если переменная в первый раз вызывается в строке 1, а в последний — в строке 25, ее время жизни равно 25 строкам. Если переменная используется только в этих двух строках, средний ин- тервал между обращениями к ней — 23 строки. Если бы между строками 1 и 25 переменная вызывалась в каждой строке, она имела бы средний интервал, равный 0, но время ее жизни по-прежнему равнялось бы 25 строкам. Связь интервалов меж- ду обращениями к переменной и времени ее жизни пояснена на рис. 10-1. Как и интервал между обращениями к переменной, время ее жизни желательно делать как можно короче. Преимущество в обоих случаях одинаково: это умень- шает окно уязвимости, снижая вероятность неверного или неумышленного изме- нения переменной между действительно нужными обращениями к ней. Второе преимущество короткого срока жизни: оно позволяет получить верное представление о коде. Если переменная изменяется в строке 10 и вызывается в строке 45, само пространство между двумя обращениями подразумевает, что пе- ременная используется также между строками 10 и 45. Если переменная изменя- ется в строке 44 и вызывается в строке 45, других обращений к ней между этими строками быть не может, что позволяет вам сосредоточиться на меньшем фраг- менте кода. Дополнительные сведения Об интервалах между обращения- ми к переменным см. работу «Software Engineering Metrics and Models» (Conte, Dunsmore, and Shen, 1986). 240 ЧАСТЬ III Переменные Рис. 10-1. «Длительное время жизни» подразумевает, что переменная используется в крупном фрагменте кода. При «коротком времени жизни» переменная используется лишь в небольшом фрагменте. «Интервал между обращениями» к переменной характеризует, насколько тесно сгруппированы обращения к переменной Короткое время жизни снижает вероятность ошибок инициализации. По мере изменения программы линейные участки кода имеют тенденцию превращаться в циклы, при этом программисты часто забывают про инициализацию перемен- ных, выполненную вдали от цикла. Поддерживая код инициализации и код цикла в непосредственной близости, вы снизите вероятность того, что изменения при- ведут к ошибкам инициализации. Кроме того, короткое время жизни облегчает чтение кода. Чем меньше строк кода нужно удерживать в уме в каждый конкретный момент времени, тем проще по- нять код. К тому же при небольшом времени жизни на экране помещаются сразу все обращения к переменной, что облегчает редактирование и отладку. Наконец, короткое время жизни облегчает разделение крупного метода на мень- шие. Если обращения к переменным сгруппированы в небольшом фрагменте, его проще выделить в отдельный метод. Оценка времени жизни переменной Время жизни переменной можно формализовать, подсчитав число строк между пер- вым и последним обращениями к ней (с учетом первой и последней строк). В следу- ющем примере каждая из переменных обладает слишком долгим временем жизни: Пример слишком долгого времени жизни переменных (Java) 1 // инициализация каждой переменной 2 recordIndex = 0; 3 total = 0; ГЛАВА 10 Общие принципы использования переменных 241 4 done = false; 26 while ( recordIndex < recordCount ) { 27 ... Последнее обращение к переменной recordIndex. 28 recordIndex = recordIndex + 1; 64 while ( !done ) { Последнее обращение к переменной total. 69 if ( total > projectedTotal ) { Последнее обращение к переменной done. 70 done = true; Времена жизни переменных: recordIndex (строка 28 - строка 2 + 1) = 27 total (строка 69 - строка 3 + 1) = 67 done (строка 70 - строка 4 + 1) = 67 Среднее время жизни (27 + 67 + 67) / 3 »54 Следующий пример аналогичен предыдущему, только теперь обращения к пере- менным сгруппированы более тесно: Пример хорошего, короткого времени жизни переменных (Java) Инициализация переменной recordIndex ранее выполнялась в строке 3. 25 recordIndex = 0; 26 while ( recordIndex < recordCount ) { 27 ... 28 recordIndex = recordIndex + 1; Инициализация переменных total и done ранее выполнялась в строках 4 и 5. 62 total = 0; 63 done = false; 64 while ( !done ) { 69 if ( total > projectedTotal ) { 70 done = true; Теперь времена жизни переменных равны: recordIndex (строка 28 - строка 25 + 1) = 4 total (строка 69 - строка 62 + 1) = 8 done (строка 70 - строка 63 + 1) = 8 Среднее время жизни (4 + 8 + 8) / 3 »7 > > > > > 242 ЧАСТЬ III Переменные Интуиция подсказывает, что второй вариант предпочтитель- нее, так как инициализация переменных выполняется бли- же к месту их использования. Сравнение среднего времени жизни переменных — 54 и 7 — подкрепляет этот интуитив- ный вывод конкретными цифрами. Какое время жизни считать приемлемым? А что можно сказать об интервале? Конк- ретных цифр у нас пока нет, но разумно предположить, что и интервал между обра- щениями к переменной, и время ее жизни следует пытаться свести к минимуму. Если в этом ключе проанализировать глобальные переменные, окажется, что им соответствует огромный средний интервал между обращениями и такое же вре- мя жизни, — это одна из многих обоснованных причин избегать глобальных пе- ременных. Общие советы по минимизации области видимости Ниже даны конкретные рекомендации по минимизации области видимости. Инициализируйте переменные, используемые в цикле, непосредственно перед циклом, а не в начале метода, содержащего цикл Следование этому совету снизит ве- роятность того, что при изменении цикла вы забудете изме- нить инициализацию используемых в нем переменных. Если же цикл будет вложен в новый цикл, переменные будут инициализироваться при каждой итерации нового цикла, а не только при первой. Не присваивайте переменной значение вплоть до его использования Вероятно, вы знаете, насколько трудно бывает найти строку, в которой переменной было присво- ено ее значение. Чем больше вы сделаете для прояснения того, где переменная получает свое значение, тем лучше. Такие языки, как C++ и Java, позволяют инициализировать переменные следующим образом: Пример грамотного объявления и инициализации переменных (C++) int receiptIndex = 0; float dailyReceipts = TodaysReceipts(); double totalReceipts = TotalReceipts( dailyReceipts ); Группируйте связанные команды В следующих фраг- ментах на примере метода, суммирующего дневную выручку, показано, как сгруппировать обращения к переменным, что- бы за ними было проще следить. В первом примере этот принцип нарушен: Пример запутанного использования двух наборов переменных (C++) void SummarizeData(...) { Перекрестная ссылка Об иници- ализации переменных около места их использования см. раздел 10.3. Перекрестная ссылка Об этом стиле объявления и определе- ния переменных см. подраздел «В идеальном случае сразу объявляйте и определяйте каж- дую переменную непосредствен- но перед первым обращением к ней» раздела 10.3. Перекрестная ссылка О группи- ровке связанных команд см. также раздел 14.2. Дополнительные сведения О времени жизни переменных см. работу «Software Engineering Met- rics and Models» (Conte, Duns- more, and Shen, 1986). ГЛАВА 10 Общие принципы использования переменных 243 Команды, в которых используются два набора переменных. GetOldData( oldData, &numOldData ); GetNewData( newData, &numNewData ); totalOldData = Sum( oldData, numOldData ); totalNewData = Sum( newData, numNewData ); PrintOldDataSummary( oldData, totalOldData, numOldData ); PrintNewDataSummary( newData, totalNewData, numNewData ); SaveOldDataSummary( totalOldData, numOldData ); SaveNewDataSummary( totalNewData, numNewData ); } Этот небольшой фрагмент заставляет следить сразу за шестью переменными: oldData, newData, numOldData, numNewData, totalOldData и totalNewData. В следу- ющем примере благодаря разделению кода на два логических блока это число снижено до трех: Пример более понятного использования двух наборов переменных (C++) void SummarizeData( ... ) { Команды, в которых используются «старые данные» (oldData). GetOldData( oldData, &numOldData ); totalOldData = Sum( oldData, numOldData ); PrintOldDataSummary( oldData, totalOldData, numOldData ); SaveOldDataSummary( totalOldData, numOldData ); Команды, в которых используются «новые данные» (newData). GetNewData( newData, &numNewData ); totalNewData = Sum( newData, numNewData ); PrintNewDataSummary( newData, totalNewData, numNewData ); SaveNewDataSummary( totalNewData, numNewData ); } Каждый из двух блоков, полученных при разделении кода, короче, чем первона- чальный блок, и содержит меньше переменных. Такой код легче понять, а если вам придется разбить его на отдельные методы, меньшие блоки, содержащие мень- шее число переменных, позволят выполнять эту задачу эффективнее. Разбивайте группы связанных команд на отдельные методы При прочих равных условиях переменная из более короткого метода обычно характеризуется меньшим интервалом между обращениями и меньшим временем жизни, чем переменная из более крупного метода. Разбиение группы связанных команд на отдельные методы позволяет уменьшить область видимости, которую может иметь переменная. Начинайте с самой ограниченной области видимос- ти и расширяйте ее только при необходимости Что- бы минимизировать область видимости переменной, поста- райтесь сделать ее как можно более локальной. Область ви- Перекрестная ссылка О гло- бальных переменных см. раздел 13.3. > > > 244 ЧАСТЬ III Переменные димости гораздо сложнее сжать, чем расширить — иначе говоря, превратить гло- бальную переменную в переменную класса сложнее, чем наоборот. Защищенные данные-члены класса также сложнее превратить в закрытые, чем закрытые в за- щищенные. Так что, если сомневаетесь, выбирайте наименьшую возможную об- ласть видимости переменной: попытайтесь сделать переменную локальной для от- дельного цикла, локальной для конкретного метода, затем — закрытой перемен- ной класса, затем — защищенной, далее попробуйте включить ее в пакет (если ваш язык программирования поддерживает пакеты) и лишь в крайнем случае сделай- те ее глобальной. Комментарии по поводу минимизации области видимости Подход к минимизации области видимости переменных часто зависит от точки зрения на вопросы «удобства» и «интеллектуальной управляемости». Некоторые программисты делают многие переменные глобальными для того, чтобы облег- чить доступ к ним и не беспокоиться о списках параметров и правилах области видимости. В их умах удобство доступа к глобальным переменным перевешивает связанную с этим опасность. Другие предпочитают делать переменные как можно более локальными, потому что локальная область видимости спо- собствует интеллектуальной управляемости. Чем больше информации вы скрыли, тем меньше вам нужно удерживать в уме в каждый конкретный момент времени и тем ниже вероятность того, что вы допустите ошибку, забыв одну из многих деталей, о которых нужно было помнить. Разница между философией «удобства» и философией «интеллектуаль- ной управляемости» сводится к различию между ориентацией на напи- сание программы и ориентацией на ее чтение. Максимизация области видимости может облегчить написание программы, но программу, в которой каж- дый метод может вызвать любую переменную в любой момент времени, сложнее понять, чем код, основанный на грамотно организованных методах. Сделав дан- ные глобальными, вы не сможете ограничиться пониманием работы одного ме- тода: вы должны будете понимать работу всех других методов, которые вместе с ним используют те же глобальные данные. Подобные программы сложно читать, сложно отлаживать и сложно изменять. Так что ограничивайте область видимости каждой перемен- ной минимальным фрагментом кода. Можете ограничить ее одним циклом или одним методом — великолепно! Не по- лучается — ограничьте область видимости методами одно- го класса. Если и это невозможно, создайте методы досту- па, позволяющие использовать переменную совместно с другими классами. «Голые» глобальные данные требуются редко, если вообще та- кое бывает. Перекрестная ссылка Идея ми- нимизации области видимости связана с идеей сокрытия ин- формации [см. подраздел «Скрывайте секреты (к вопро- су о сокрытии информации)» раздела 5.3]. Перекрестная ссылка О методах доступа см. подраздел «Исполь- зование методов доступа вме- сто глобальных данных» разде- ла 13.3. |