Введение
Эта статья посвящена таким вопросам программирования как стили форматирования, именование переменных, читабельность кода итд. Статья написана в форме правил, которых следует придерживаться. Каждое такое правило проиллюстрировано кодом на языке Си. Я думаю, эти советы будут полезны в первую очередь начинающим программистам. Крайне важно уже в начале пути выработать хорошие привычки. Если же вы матерый программист, то врятли вы найдете здесь что-то новое - таким я советовал бы в первую очередь обратиться трудам Макконелла, в которых вы сможете найти более детальное и глубокое описание изложенных здесь проблем.
ПеременныеЧем меньше время жизни переменной, тем лучше
Жизнь переменной начинается после первого к ней обращения и заканчивается после последнего. Уменьшение времени жизни сокращает вероятность случайной модификации переменной, а также увеличивает ясность кода. Если пространство между двумя переменными большое это подразумевает, что она еще гдето использовалась.
Одна переменная для одной цели!Хорошо 15:return_value = TIMEDOUT; ... 25:return_valie = OK; !Плохо 15:return_value = TIMEDOUT; ... 150:return_valie = OK;
Иногда довольно сложно удержаться от соблазна создать переменную temp и использовать ее при каждом удобном случае. В следующем примере происходит обмен значений двух переменных:
!Плохо temp = filename[1]; filename[1] = filename[2]; filename[2] = temp; ... temp = port[1]; port[1] = port[2]; port[2] = temp; !Хорошо old_filename = filename[1]; filename[1] = filename[2]; filename[2] = old_filename; ... old_port = port[1]; port[1] = port[2]; port[2] = old_port;
Взглянув на первый пример можно сделать вывод, что переменные directory и filename как-то связанны, что вводит в заблуждение.
C подозрением относитесь к глобальным переменнымБольшое количество глобальных переменных должно заставить вас задуматься об архитектуре вашей программы. Глобальные переменные прежде всего плохи тем что не отвечают правилу уменьшения времени жизни переменной, со всеми вытекающими последствиями. Конечно, не стоит фанатично истреблять все глобальные переменные, но задуматься над уменьшением их количества определенно стоит. Если вы используете глобальные переменные то выберете для них прификс, например g_, что позволит вам более тщательно отслеживать их применение.
Используйте директиву #define!Плохо ... error = 1234; ... И как понять что обозначает error 1234? !Хорошо ... #define CANT_OPEN_FILE 1234 ... error = CANT_OPEN_FILE; ... Ну теперь можно предположить что произошла ошибка открытия файла.
Используя #define мы убили сразу двух зайцев. Во-первых повысили ясность кода а во вторых избавили себя от необходимости лазить по всему исходнику в поисках 1234, если изменится код ошибки.
Обьявляйте строки как MAXLINE + 1Многие программисты забывают, что такие функции как strcpy, strcat итд не заботятся о добавлении нуль байта в конец строки. Это чревато однобайтовым переполнением и другими ошибками. Объявляя строку как MAXLINE + 1 мы заботимся о нулевом байте.
Чаще пользуйтесь структурами!Плохо char buf[MAXLINE]; strncpy(buf, evil_string, MAXLINE); !Хорошо char buf[MAXLINE+1]; strncpy(buf, evil_string, MAXLINE);
Допустим у нас есть данные о работниках фирмы - name, address, phone, gender, salary. Имеет смысл объединить их в структуру. Если эти данные передаются в функцию то гораздо удобнее передать саму структуру, чем писать все эти поля.
Тщательно выбирайте имена переменных.!хорошо ... struct employee { char name[MAXLINE+1]; char address[MAXLINE+1]; char phone[MAXLINE+1]; char gender[MAXLINE+1]; char sakary[MAXLINE+1]; }; ... int check_work(struct employee *);
Выбирая имя переменной нужно прежде всего позаботиться о том чтобы оно выражало суть этой переменной. Часто хорошим именем является словесное описание значения переменной.
!Плохо x = sqrt(b*b - 4ac); !Хорошо discriminant = sqrt(b*b - 4ac);
Для именования индексов циклов обычно применяются i,j,k. Если переменная являющаяся индексом цикла будет применяться после завершения цикла, следует выбрать более подходящее имя для нее.
Правильно именуйте флаги.!Плохо while (i < target_port) { ... i++; } ... remoute_port = i; !Хорошо while (current_port < target_port) { ... current_port++; } ... remoute_port = current_port;
Не присваивайте переменной статуса имя flag. Выбирайте яркие имена для флагов.
Операторы!Плохо if (sock == CONNECT_FLAG) { ... } /* Здесь непонятно что значит CONNECT_FLAG. Произошло соединение или нет? */ if (sock == ERROR_ONE_FLAG) { ... } if (sock == ERROR_TWO_FLAG) { ... } /* Здесь можно догадаться что происходят какие-то ошибки, но вот какие остается загадкой. !Хорошо if (sock == CONNECT_OK) { ... } /* Ну вот сейчас уже можно сказать что соединение установлено */ if (sock == CANT_READ) { ... /* ошибка чтения */ } if (sock == CANT_WRITE) { ... } /* ошибка записи */
Группируйте взаимосвязанные элементы.
Гораздо удобнее работать с кодом, где модификация одних данных производится в одном месте а не разбросаны по всему коду без веской на это причины.
Размещайте нормальный вариант после if, а не после else!Плохо 25: strncpy(employee.name, current_name, MAXLINE); ... 80: strncpy(employee.name, current_phone, MAXLINE); !Хорошо 25: strncpy(employee.name, current_name, MAXLINE); 26: strncpy(employee.name, current_phone, MAXLINE);
Пользуйтесь скобками!Плохо if (sock == CANT_CONNECT) { ... } else { ... } !Хорошо if (sock == CONNECT_OK) { ... } else { ... }
Ненужно опускать скобки { } даже если это допустимо со стороны синтаксиса. Использование скобок делает код более удобочитаемым. Вполне возможно что в будущем придется модифицировать конструкцию, добавляя новые операторы. Отсутствие скобок в этом случае может вызвать довольно неприятные ошибки. Например:
!Было ... if( status == OK) make_deal(); !Стало if( status == OK) make_deal(); check_sock(); !А должно было стать if( status == OK) { make_deal(); check_sock(); }
Когда вы начнете искать ошибку в коде вполне вероятно, что из-за обманчивого форматирования, вы ее не увидите. Таким образом можно cделать вывод:
Избегайте пустых блоков в конструкциях if-else!Плохо for (i = 1; i < MAX_VALUE; i++) for(j = 1; j < MAX_VALUE; j++) for(k = 1; k < MAX_VALUE; k++) ... !Хорошо for (i = 1; i < MAX_VALUE; i++) { for(j = 1; j < MAX_VALUE; j++) { for(k = 1; k < MAX_VALUE; k++) { ... } } }
Упрощайте проверки в операторах if-else!Плохо if (FILE_OPENED) ; else { /* делаем что-то если файл не открыт */ } !Хорошо if ( ! FILE_OPENED) { /* делаем что-то если файл не открыт */ }
Допустим есть символ, нам нужно отнести этот символ к одной из групп: знаки применания, цифры и буквы. Смотрим возможные реализации:
!Плохо if ( input_character == " " || input_character == "," || input_character == "." || input_character == "!" || input_character == "(" || input_character == ")" || input_character == ":" || input_character == ";" || input_character == "?" || input_character == "-" || ) { character_type = character_type_punctuation; } else if ( "0" <= input_character && input_character <= "9") { character_type = character_type_digit; } else if ( "a" <= input_character && input_character <= "z" || "A" <= input_character && input_character <= "Z") { character_type = character_type_letter; }
А плохо это потому, что следует избегать таких громоздких условий, которые затрудняют прослеживании общей логики программы.
Используя конструкцию if-else следует выносить вперед варианты, встречающиеся чаще других.!Хорошо if ( is_punctuation(input_character)) { character_type = character_type_punctuation; } else if ( is_digit(input_character)) { character_type = character_type_digit; } else if ( is_letter(input_character)) { character_type = character_type_letter; }
Это увеличит производительность кода, благодаря уменьшению проверок. Например, если мы уверенны что наибольшую вероятность встретиться в тексте имеют буквы, а следом за ними символы и цифры то:
Улучшайте читабельность в конструкции switch-case!Плохо if ( is_punctuation(input_character)) { character_type = character_type_punctuation; } else if ( is_digit(input_character)) { character_type = character_type_digit; } else if ( is_letter(input_character)) { character_type = character_type_letter; } !Хорошо if ( is_letter(input_character)) { character_type = character_type_letter; } else if ( is_punctuation(input_character)) { character_type = character_type_punctuation; } else if ( is_digit(input_character)) { character_type = character_type_digit; }
Этого можно добиться двумя способами. Во первых упорядочив перечисление например по алфавиту. Если обрабатывается много условий то такой подход наверяка упростит поиск необходимого варианта.
Не изменяйте переменную цикла for в его теле.!Хорошо switch (action) { case "a": ... break; case "b": ... break; case "c": ... break; ... case "y": ... break; default: ... } !Плохо switch (action) { case "c": ... break; case "a": ... break; case "e": ... break; ... case "y": ... break; default ... }
Это является плохим тоном.
!Плохо for ( i = 1, i < 100; i++) { ... if (...) { ... i = 100; } }
Если вам необходимо выйти из цикла до его завершения воспользуйтесь такой конструкцией:
Пытайтесь уменьшить длину цикла.!Хорошо for ( i = 1, i < 100; i++) { ... if (...) { ... break; } }
Это поможет лучше понять логику его выполнения.
!Плохо for ( i = 1, i < MAX_VALUE, i++) { ...50 строк кода... for ( j = 1, j < MAX_VALUE, j++) { ...60 строк кода... for ( k = 1, k < MAX_VALUE, k++) { ... } } }
Предпочтительнее вынести эти ...n строк кода... в отдельные функции.
Стили форматирования!Хорошо for ( i = 1, i < MAX_VALUE, i++) { one(); for ( j = 1, j < MAX_VALUE, j++) { two(); for ( k = 1, k < MAX_VALUE, k++) { ... } } }
Главной задачей форматирования является сделать код более читабельным. Различие разных стилей в формировании отступов, расстановки скобок, оформлении операторов. Сейчас широкое распространение имеют следующие:
Стиль K&R - назван так потому что именно в таком стиле написаны все примеры к книгам Кернигана и Ричи. Иногда называют kernel style из-за того, что ядро UNIX написано именно в этом стиле.
if (...) { ......../* отступ на таб или 8 пробелов */ ......../* код */ }
Стиль Олмана - назван так в честь Эрика Олмана, написавшего много утилит для BSD. Еще известен как bsd style.
if (...) { ......../* отступ на таб или 8 пробелов */ }
Whitesmith - назван так из-за того, что использовался в ide компилятора whitesmith c.
if (...) ........{ ......../* отступ на таб или 8 пробелов */ ......../* код */ .........}
Стиль GNU - назван так из-за использования в редакторе Emacs и некоторых других GNU утилит.
if (...) ..{ /* отступ на 2 пробела */ ..../* отступ на 4 пробела */ ..../* код */ ..}
Чтобы вникнуть в детали понравившегося стиля советую вам обратиться к программам, написанным на нем. Выберете более удобочитаемый для вас.
ЗаключениеПрежде чем кидаться за реализацию стоит спокойно сесть и набросать примерный план программы. Хорошее планирование будущей архитектуры избавит вас от множества проблем на стадии реализации. Если перед вами стоит задача написать программу из 10 строк кода то врятли вы задумаетесь об именовании переменных, группировке элементов и других методиках, позволяющих упростить код. Он и так прост. Но что вы будите делать если реализация поставленной задачи еле-еле убирается в 50 000 строк?
При написании кода задавайте себе вопрос - сможет ли другой человек понять его? Даже если вы не предполагаете распространение исходников, такой подход значительно упростит вашу задачу, если например спустя пол года вам понадобится вернуться к этой программе. Чем лучше продуманна структура программы, тем проще вам будет. Идеальный вариант - если при написании программы, в определенный момент времени, вы будите думать только об одной маленькой части. В этом случае вам будет безразлично - 10 или 10 000 строк вам нужно написать. Так давайте не будем усложнять себе жизнь.
© Sun Inventor 14.05.2007
Стоит добавить что это статья для Си. Для С++ если же это применять - это будет полный ахтунг, как для вариант хорошо так и для варианта плохо.

Кулл стотья в будушем понадобится.