Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 31 Форматирование и стиль 741 матирования многострочных вызовов методов работает хорошо при последова- тельном применении. При переносе строк в управляющем выражении делайте отступ стандар- тного размера При нехватке места для цикла for, цикла while или оператора if, перенося строку, сделайте такой же отступ, как и в выражениях внутри цикла или после условия if. В листинге 31-45 приведены два примера: Листинг 31-45. Примеры отступов при переносе строк в управляющих выражениях (Java) while ( ( pathName[ startPath + position ] != ‘;’ ) && В продолжении строки отступ содержит стандартное количество пробелов... ( ( startPath + position ) <= pathName.length() ) ) { } for ( int employeeNum = employee.first + employee.offset; ...так же, как и в этом случае. employeeNum < employee.first + employee.offset + employee.total; employeeNum++ ) { } Приведенный код соответствует критериям, установленным ранее в этой главе. Перенос строки выполняется логично — в нем всегда есть отступ относительно выражения, кото- рое он продолжает. Дальнейшее выравнивание может вы- полняться как обычно — новая строка занимает всего на несколько пробелов больше, чем исходный вариант. Он так же читаем, как и любой другой код, и его так же просто сопровождать. В некоторых случаях вы могли бы повысить удобочитаемость, на- строив отступы или пробелы, но, рассматривая вопросы тонкой настройки, не забывайте о компромиссе с точки зрения удобства сопровождения. Не выравнивайте правые части выражений присваивания В первом из- дании этой книги я рекомендовал выравнивать правые части выражений, содер- жащие присваивания (листинг 31-46): Листинг 31-46. Примеры форматирования в конце строки, используемого при выравнивании выражений присваивания, — плохая практика (Java) customerPurchases = customerPurchases + CustomerSales( CustomerID ); customerBill = customerBill + customerPurchases; totalCustomerBill = customerBill + PreviousBalance( customerID ) + LateCharge( customerID ); customerRating = Rating( customerID, totalCustomerBill ); Перекрестная ссылка Иногда для сложных условий лучше всего поместить их в логичес- кие функции (см. подраздел «Упрощение сложных выраже- ний» раздела 19.1). > > 742 ЧАСТЬ VII Мастерство программирования С высоты 10-летнего опыта могу сказать, что, хотя этот стиль может выглядеть привлекательно, он превращается в настоящую головную боль, когда приходится поддерживать выравнивание знаков равенства при изменении имен переменных и прогоне кода через утилиты, заменяющие пробелы знаками табуляции, а знаки табуляции — пробелами. Кроме того, его тяжело сопровождать при перемещении строк между частями программы, в которых применяются разные размеры отступов. Для обеспечения единообразия с другими принципами отступов, а также учиты- вая вопросы удобства сопровождения, обращайтесь с группами выражений, со- держащими операции присваивания, так же, как вы бы обращались с любыми другими выражениями (листинг 31-47): Листинг 31-47. Пример использования стандартных отступов для выравнивания выражений присваивания — хорошая практика (Java) customerPurchases = customerPurchases + CustomerSales( CustomerID ); customerBill = customerBill + customerPurchases; totalCustomerBill = customerBill + PreviousBalance( customerID ) + LateCharge( customerID ); customerRating = Rating( customerID, totalCustomerBill ); При переносе строк в выражениях присваивания применяйте отступы стандартного размера В листинге 31-47 при переносе строки в третьем при- сваивании используется стандартный размер отступа. Это сделано с той же це- лью, что и отказ от специального форматирования выражений присваивания: из общих соображений читаемости и удобства сопровождения. Размещение одного оператора на строке Современные языки, такие как C++ и Java, позволяют располагать несколько опе- раторов на одной строке. Однако когда дело касается этого вопроса, мощь сво- бодного форматирования оборачивается палкой о двух концах. Следующая стро- ка содержит несколько выражений, которые, с точки зрения логики, вполне мо- гут располагаться в отдельных строках: i = 0; j = 0; k = 0; DestroyBadLoopNames( i, j, k ); Аргументом в защиту размещения нескольких выражений на одной строке может служить факт, что в этом случае требуется меньшее число строк экранного про- странства или бумаги для распечатки, что позволяет одновременно видеть боль- ший объем кода. Это также позволяет сгруппировать взаимосвязанные выраже- ния, а некоторые программисты даже полагают, что так они подсказывают ком- пилятору, как можно оптимизировать код. Все так, но основания для самоограничения, требующие оставлять не более од- ного оператора в строке, гораздо серьезней. 쐽 Размещение каждого оператора на отдельной строке дает точное представле- ние о сложности программы. При этом не скрывается сложность из-за того, что сложные операторы выглядят тривиальными. Сложные операторы и вы- глядят сложными, простые — простыми. ГЛАВА 31 Форматирование и стиль 743 쐽 Размещение нескольких операторов на одной строке не помогает современным компиляторам в оптимизации. Сегодняшние оптимизирующие компиляторы не нужда- ются в подсказках, сделанных с помощью форматирова- ния (см. ниже). 쐽 Если операторы расположены на отдельных строках, чтение кода происходит сверху вниз, а не сверху вниз и слева направо. При поиске определенной строки у взгляда должна быть возможность придерживаться левого края кода. Он не должен просматривать каждую строку целиком только потому, что в одной строке может быть два оператора. 쐽 При размещении операторов на отдельных строках легко найти синтаксичес- кие ошибки, если компилятор сообщает только номера строк, где они произош- ли. При расположении нескольких операторов на одной строке ее номер ни- чего не скажет о том, какой оператор содержит ошибку. 쐽 При размещении операторов на отдельных строках легко выполнять пошаго- вую отладку кода, используя построчные отладчики. Если строка содержит несколько операторов, отладчик выполнит их все одновременно, и вам при- дется переключиться на ассемблерный листинг для выполнения пошаговой отладки отдельных выражений. 쐽 Когда строка содержит только один оператор, его легко редактировать — можно удалить или временно закомментировать всю строку. Если же на одной стро- ке вы разместили несколько операторов, вам придется выполнять редактиро- вание между остальными операторами. В C++ избегайте выполнения нескольких операций в одной строке (побоч- ные эффекты) Побочные эффекты — это последствия выполнения некоторо- го выражения, проявляющиеся в дополнение к основным результатам выполне- ния этого выражения. Так, в C++ оператор ++, расположенный на одной строке с другими операторами, приводит к проявлению побочного эффекта. Присваива- ние значения переменной и применение левой части этого присваивания в ус- ловном операторе также является примером побочного эффекта. Побочные эффекты снижают читаемость кода. Например, если n равно 4, что напечатает выражение, приведенное в листинге 31-48? Листинг 31-48. Пример непредсказуемого побочного эффекта (C++) PrintMessage( ++n, n + 2 ); 4 и 6? Или 5 и 7? А может, 5 и 6? Правильный ответ: «Ни то, ни другое и не тре- тье». Первый аргумент — ++n — равен 5. Но язык C++ не определяет порядок вы- числения условий выражения или аргументов функции. Поэтому компилятор может вычислить второй аргумент, n + 2, либо до, либо после первого аргумента, и ре- зультат может быть равен 6 или 7 в зависимости от компилятора. В листинге 31- 49 показано, как переписать это выражение, чтобы прояснить свои намерения: Перекрестная ссылка Об опти- мизации производительности на уровне кода см. главы 25 и 26. 744 ЧАСТЬ VII Мастерство программирования Листинг 31-49. Пример избавления от непредсказуемого побочного эффекта (C++) ++n; PrintMessage( n, n + 2 ); Если вы все еще не совсем уверены в том, что побочные эффекты надо выносить в отдельные строки, попробуйте понять, что делает функция, приводимая в лис- тинге 31-50: Листинг 31-50. Пример слишком большого количества операции в строке (C) strcpy( char * t, char * s ) { while ( *++t = *++s ) ; } Некоторые опытные программисты не видят сложности в этом примере, потому что эта функция им знакома. Они смотрят на нее и говорят: «Это функция strcpy()». Однако в нашем случае это не совсем strcpy(). Она содержит ошибку. Если вы ска- зали «Это strcpy()», увидев данный код, вы узнали код, а не прочитали его. В такую же ситуацию вы попадаете при отладке программы: код, на который вы не обрати- ли внимания, потому что «узнали», а не прочли его, может содержать ошибку, по- иск которой займет гораздо больше времени, чем она этого заслуживает. Фрагмент, показанный в листинге 31-51, функционально идентичен первому ва- рианту и гораздо удобней для чтения: Листинг 31-51. Пример читаемого количества операций в каждой строке (C) strcpy( char * t, char * s ) { do { ++t; ++s; *t = *s; } while ( *t != ‘\0’ ); } В этом переформатированном коде ошибка очевидна. Конечно, t и s инкремен- тируются до того, как *s будет скопирована в *t. Первый символ пропускается. Второй пример выглядит продуманней первого, хотя операции, выполняемые во втором примере, идентичны первому. Причина такого впечатления в том, что во втором варианте не скрывается сложность выполняемых действий. Рост производительности также не оправдывает размеще- ния нескольких операций на одной строке. Поскольку обе функции strcpy() логически эквивалентны, можно ожидать, что компилятор сгенерирует для них идентичный код. Од- нако при профилировании обеих функций выяснилось, что для копирования 5 000 000 строк первой функции понадобилось 4,81 секунды, а второй —4,35. В нашем случае «умная» версия показала снижение скорости на 11%, что делает ее гораздо менее умной. Результаты могут изменяться от компилятора к компиля- Перекрестная ссылка О на- стройке кода см. главы 25 и 26. ГЛАВА 31 Форматирование и стиль 745 тору, но в целом они свидетельствуют о том, что пока вы не измерили прирост производительности, следует сначала стремиться к ясности и корректности, а уж затем — к производительности. Даже если вы легко читаете выражения с побочными эффектами, пожалейте тех, кому придется разбираться с вашим кодом. Большинству программистов нужно дважды подумать, чтобы понять выражения с побочными эффектами. Позвольте им использовать мозговые клетки для осмысления более общих вопросов рабо- ты вашего кода, а не синтаксических деталей конкретного выражения. Размещение объявлений данных Располагайте каждое объявление данных в отдельной строке Как показали предыдущие примеры, каждому объявлению данных надо выделять отдельную строку. Тог- да проще дописывать комментарии к каждому объявлению — ведь каждое расположено в отдельной строке. Проще ис- правлять объявления, поскольку все они изолированы друг от друга. Проще находить конкретные переменные, так как можно просканировать одну колонку, а не читать каждую строчку. Проще искать и исправлять синтаксические ошибки — строка, указанная компилятором, содер- жит только одно объявление. А ну-ка, скажите быстренько: какой тип имеет переменная currentBottom в объяв- лении данных, приведенном в листинге 31-52? Листинг 31-52. Пример скопления нескольких объявлений переменных в одной строке (C++) int rowIndex, columnIdx; Color previousColor, currentColor, nextColor; Point previousTop, previousBottom, currentTop, currentBottom, nextTop, nextBottom; Font previousTypeface, currentTypeface, nextTypeface; Color choices[ NUM_COLORS ]; Это, конечно, крайний случай, однако он не так уж далек от гораздо более рас- пространенного стиля, показанного в листинге 31-53: Листинг 31-53. Пример скопления нескольких объявлений переменных в одной строке (C++) int rowIndex, columnIdx; Color previousColor, currentColor, nextColor; Point previousTop, previousBottom, currentTop, currentBottom, nextTop, nextBottom; Font previousTypeface, currentTypeface, nextTypeface; Color choices[ NUM_COLORS ]; Это не такой уж и редкий стиль объявления переменных, а конкретную перемен- ную все так же тяжело найти, поскольку все объявления свалены в кучу. Тип пере- менной тоже тяжело выяснить. А теперь скажите, какой тип имеет nextColor в листинге 31-54? Перекрестная ссылка О доку- ментировании объявлений дан- ных см. подраздел «Комменти- рование объявлений данных» раздела 32.5. Об использовании данных см. в главах 10-13. 746 ЧАСТЬ VII Мастерство программирования Листинг 31-54. Пример читаемого кода, достигнутого благодаря размещению только одной переменной в каждой строке (C++) int rowIndex; int columnIdx; Color previousColor; Color currentColor; Color nextColor; Point previousTop; Point previousBottom; Point currentTop; Point currentBottom; Point nextTop; Point nextBottom; Font previousTypeface; Font currentTypeface; Font nextTypeface; Color choices[ NUM_COLORS ]; Вероятно, переменную nextColor было проще найти, чем nextTypeface в листинге 31-53. Такой стиль характеризуется наличием в каждой строке одного полного объявления, включающего тип переменной. Надо признать, что такой стиль съедает больше экранного пространства — 20 строк, а не 3, как в первом примере, хотя те три строки и выглядели довольно безобраз- но. Я не могу процитировать ни одного исследования, показывающего, что этот стиль приводит к меньшему количеству ошибок или к лучшему пониманию про- граммы. Однако если Салли попросит меня посмотреть ее код, а объявления дан- ных будут выглядеть, как в первом примере, я отвечу: «Ни за что — это читать невозможно». Если они будут выглядеть, как во втором примере, я скажу: «Гм… Может, попозже». А если они будут выглядеть, как в третьем примере, я скажу: «Конечно, с удовольствием!» Объявляйте переменные рядом с местом их первого использования Еще более предпочтительный вариант, чем объявление всех переменных в одном боль- шом блоке, — это стиль, при котором переменная объявляется рядом с местом ее первого использования. Это уменьшает срок службы и время жизни переменной и позволяет проще выполнить рефакторинг кода на меньшие методы (см. подраздел «Делайте время жизни переменных как можно короче» раздела 10.4). Разумно упорядочивайте объявления В листинге 31-54 объявления сгруппи- рованы по типам. Такая группировка обычно имеет смысл, поскольку перемен- ные одинаковых типов часто используются в аналогичных операциях. В других случаях можно упорядочивать их по алфавиту в соответствии с именами перемен- ных. Хотя у алфавитной сортировки много сторонников, мне кажется, затрачи- ваемых не нее усилий она не стоит. Если ваш список переменных настолько дли- нен, что ему помогает алфавитное упорядочиение, ваш метод, вероятно, слишком велик. Разбейте его на части, чтобы создать меньшие по размеру методы с неболь- шим количеством переменных. В C++ при объявлении указателей располагайте звездочку рядом с именем переменной или объявляйте типы-указатели Очень часто приходится ви- ГЛАВА 31 Форматирование и стиль 747 деть объявления указателей, в которых звездочка расположена рядом с типом, как показано в листинге 31-55: Листинг 31-55. Примеры расположения звездочек в объявлениях указателей (C++) EmployeeList* employees; File* inputFile; При размещении звездочки рядом с именем типа, а не переменной возникает опасность, что в случае добавления нового объявления в той же строке звездочка будет относиться только к первой переменной, хотя визуальное форматирование наводит на мысль, что она применяется ко всем переменным в строке. Вы може- те решить эту проблему, поместив звездочку рядом с именем переменной, а не с именем типа (листинг 31-56): Листинг 31-56. Пример расположения звездочек в объявлениях указателей (C++) EmployeeList *employees; File *inputFile; Недостаток такого подхода в том, что в этом случае звездочка может казаться частью имени переменной, а это не соответствует действительности. Переменную мож- но применять как со звездочкой, так и без нее. Наилучший вариант — объявление и использование типа-указателя (листинг 31-57): Листинг 31-57. Пример правильного применения типа-указателя в объявлениях (C++) EmployeeListPointer employees; FilePointer inputFile; Указанную проблему можно решить, либо требуя объявления всех указателей с помощью типов-указателей (листинг 31-57), либо запрещая размещение более од- ной переменной в строке. Убедитесь, что применяете хотя бы одно из этих реше- ний! 31.6. Размещение комментариев Хорошо оформленные комментарии могут значительно улуч- шить читаемость программы; плохо — сильно ей повредить. Делайте в комментарии такой же отступ, как и в соответствующем ему коде Визуальные отступы вно- сят важный вклад в понимание логической структуры программы, и хорошие ком- ментарии не мешают визуальному выравниванию. Например, что можно сказать о логической структуре метода, приведенного в листинге 31-58? Листинг 31-58. Пример комментариев с неправильными отступами (Visual Basic) For transactionId = 1 To totalTransactions ’ получаем информацию о транзакции Перекрестная ссылка О других аспектах комментариев см. гла- ву 32. 748 ЧАСТЬ VII Мастерство программирования GetTransactionType( transactionType ) GetTransactionAmount( transactionAmount ) ’ обрабатываем транзакцию в зависимости от ее типа If transactionType = Transaction_Sale Then AcceptCustomerSale( transactionAmount ) Else If transactionType = Transaction_CustomerReturn Then ’ процесс либо оформляет автоматический возврат, либо в случае необходимости ’ ждет подтверждения от менеджера If transactionAmount >= MANAGER_APPROVAL_LEVEL Then ’ пытаемся получить подтверждение от менеджера, а затем оформляем или отменяем ’ возврат средств в зависимости от полученного подтверждения GetMgrApproval( isTransactionApproved ) If ( isTransactionApproved ) Then AcceptCustomerReturn( transactionAmount ) Else RejectCustomerReturn( transactionAmount ) End If Else ’ подтверждение менеджера не требуется, поэтому оформляем возврат AcceptCustomerReturn( transactionAmount ) End If End If End If Next В этом примере вам не удастся сходу разобраться в логической структуре кода, так как комментарии полностью скрывают его визуальный формат. Вероятно, тяжело поверить, что кто-то в здравом уме решит использовать такой стиль от- ступов, но я неоднократно встречал его в профессиональных программах и знаю минимум один учебник, рекомендующий его. Код, приведенный в листинге 31-59, абсолютно идентичен предыдущему приме- ру из листинга 31-58 за исключением отступов в комментариях. Листинг 31-59. Пример комментариев с правильными отступами (Visual Basic) For transactionId = 1 To totalTransactions ‘ получаем информацию о транзакции GetTransactionType( transactionType ) GetTransactionAmount( transactionAmount ) ‘ обрабатываем транзакцию в зависимости от ее типа If transactionType = Transaction_Sale Then AcceptCustomerSale( transactionAmount ) |