CHAP5 (1018805)
Текст из файла
Глава
5
Правила обычного программирования
Эта часть содержит правила, относящиеся к написанию собственно исходного текста программы, в отличие от предыдущей части, в которой рассматривалась разработка программы в целом. Эти правила не слишком зависят от выбора языка программирования.
50. Не путайте привычность с читаемостью.
(Или синдром "настоящего программиста, который может программировать на любом языке как на ФОРТРАНе"). Многие люди пытаются злоупотреблять препроцессором для того, чтобы придать С большее сходство с каким-нибудь другим языком программирования. Например:
#define begin {
#define end }
while ( ... )
begin
// ...
end
Эта практика ничего не дает, кроме того, что ваш код становится нечитаемым для кого-нибудь, кто не знает того языка, который вы стараетесь имитировать. Для программиста на С код станет менее читаемым, не более того.
Родственная проблема связана с использованием макросов препроцессора для скрытия синтаксиса объявлений С. Например, не делайте следующего:
typedef const char *LPCSTR;
LPCSTR str;
Подобные вещи вызывают проблемы с сопровождением, потому что кто-то, не знакомый с вашими соглашениями, будет должен просматривать typedef, чтобы разобраться, что происходит на самом деле. Дополнительная путаница возникает в С++, потому что читатель может интерпретировать происходящее, как определение объекта С++ из класса LPCSTR. Большинству программистов на С++ не придет в голову, что LPCSTR является указателем. Объявления С очень легко читаются программистами на С. (Кстати, не путайте вышеупомянутое с разумной практикой определения типа word в виде 16-битового числа со знаком для преодоления проблемы переносимости, присущей типу int, размер которого не определен в ANSI С и С++).
К тому же, многие программисты избегают условной операции (?:) просто потому, что она им кажется непонятной. Тем не менее, это условное выражение может существенно упростить ваш код и, следственно, сделать его лучше читаемым. Я думаю, что:
printf("%s", str ? str : "<пусто>");
гораздо элегантнее, чем:
if ( str == NULL )
printf( "<пусто>" );
else
printf( "%s", str );
Вы к тому же экономите на двух вызовах printf(). Мне также часто приходится видеть неправильное использование операций ++ и --. Весь смысл автоинкремента или автодекремента заключается в соединении этих операций с другими. Вместо:
while ( *p )
{
putchar ( *p );
++p;
}
или:
for ( ; *p ; ++p )
putchar ( *p );
используйте:
while ( *p )
putchar ( *p++ );
Этот код вполне читаем для компетентного программиста на языке С, даже если ему нет эквивалентной операции в ФОРТРАНе или Паскале.
Вы также никогда не должны прятать операторы в макросах из-за того, что вам просто не нравится, как они выглядят. Я однажды видел следующее в реальной программе:
struct tree_node
{
struct tree_node *lftchld;
};
#define left_child(x) ((x)->lftchld)
//...
traverse( tree_node *root )
{
if ( left_child(root) )
traverse( left_child( root ) );
// ...
}
Программист намеренно сделал определение структуры труднее читаемым для того, чтобы избежать конфликта имен между полем и совершенно необходимым макросом, и все из-за того, что ему не нравился внешний вид оператора ->. Для него было бы гораздо лучшим выходом просто назвать поле left_child и совсем избавиться от макроса.
Если вы действительно думаете, что программа должна внешне выглядеть как на Паскале, чтобы быть читаемой, то вы должны программировать на Паскале, а не на С или С++.
51. Функция должна делать только одно дело.
Это обычно не очень удачная мысль - записывать то, что должна делать функция, через ее аргументы. Это должно делать имя функции. Например:
UpdateAllViews( CView *sender, long lhint, CObject *phint )
{
| // sender | lhint | phint | |
| // NULL | xx | xx | Начальное обновление, вызываемое из обрамляющего окна |
| // Cview* // // | 0 | CRect* | Вызывается, когда встроенный объект становится действительным. phint указывает на прямоугольник документа, сохраняя положение недействительного объекта |
| // Cview* // // | 1 | CRect* | Сообщение, посылаемое объектом CView* ("sender" - передатчик). phint сохраняет для CView* обрамляющее окно его клиента. |
| // ... |
}
Вам нужны вместо этого три функции: initial_update(), update_embedded_object() и update_view(). Верным ключом для понимания того, что здесь что-то не так, является туманная природа имен аргументов. Функции не должны передаваться "намеки". Ей должны даваться указания.
52. Иметь слишком много уровней абстракции или инкапсуляции так же плохо, как и слишком мало.
Основной смысл использования таких абстракций, как функции или символьные константы (или инкапсуляций, подобных определениям struct или class), заключается в улучшении читаемости программ. Не пользуйтесь ими просто потому, что вы можете делать это. Например, вложенные структуры в данном фрагменте не служат какой-либо полезной цели:
struct tree_node;
struct child_ptr
{
unsigned is_thread;
struct tree_node *child;
};
struct tree_node
{
struct child_ptr left,
right;
};
tree_node *p;
if ( !p->left.am_a_thread )
p = p->left.child;
Следующий код лучше читается, потому что в нем меньше точек, и легче сопровождается, так как в нем нужно отлаживать на одно определение меньше:
struct tree_node
{
struct tree_node *left_child;
unsigned left_is_thread : 1;
struct tree_node *right_child;
unsigned right_is_thread : 1;
};
if ( !p->left_is_thread )
p = p->left_child;
53. Функция должна вызываться более одного раза, но...
Кроме того, если функция должным образом связана (т.е. если она выполняет единственную операцию и весь код функции работает на ее результат), то нет причины извлекать кусок кода в другие функции, если только вы не желаете использовать эту часть кода где-то еще. Мой опыт говорит, что когда функция становится слишком большой, то часто возможно выделить куски, которые обладают достаточной общностью, чтобы быть использованными где-либо еще в программе, так что это правило на самом деле не противоречит правилу "маленькое - прекрасно". Если вы не выделяете этот код, то блочный комментарий, описывающий назначение этого блока программы, который вы могли бы выделить, служит той же самой цели, что и имя функции - документированию.
Тем не менее, иногда выделение кода в функции меньшего размера существенно улучшает читаемость этого кода по причине устранения беспорядка. Однако эта практика - создание абстракции для части кода в виде имени функции - добавляет идентификаторы в область глобальных имен, и эта возросшая общая сложность является определенным минусом. Если я использую функцию этим способом - как абстракцию -, то обычно обяъвляю ее одновременно статической, чтобы к ней нельзя было получить доступ снаружи текущего файла, и встроенной, чтобы ее вызов не приводил к накладным расходам. Не доводите процесс функционального абстрагирования до крайности. Мне доводилось видеть отличные программы, доведенные абстрагированием до полностью нечитаемого состояния такой степени, что нет ни одной функции длиннее, чем 5 или 6 строк. Получающаяся программа работает также значительно медленнее, чем необходимо, и ее исходный текст в 5 раз длиннее, чем нужно.
53.1. Код, используемый более одного раза, должен быть помещен в функцию.
Это правило является обратной стороной предыдущего. Если вы обнаруживаете почти идентичный код появляющимся более чем в одном месте своей программы, то этот код должен выделен в подпрограмму, которая вызывается из нескольких мест. Выгода состоит в меньшем размере программы и лучшей сопровождаемости вследствие упрощения программы и того, что вы должны теперь сопровождать лишь одну функцию; если вы находите ошибку, то вам нужно исправить ее только в одном месте. Как было упомянуто ранее, имя функции также дает хорошую абстракцию. Вызовы функции с хорошо выбранным именем являются обычно самодокументирующимися, устраняя необходимость в комментариях.
54. Функция должна иметь лишь одну точку входа.
Это правило применимо лишь к программам на С. Вообще, множество переходов goto к одной точке выхода лучше, чем много операторов return. Этим способом вы можете поместить точку прерывания у единственной точки выхода, вместо того чтобы возиться с несколькими прерываниями. Например:
f()
{
int возвращаемое_значение = ОШИБКА;
if ( некое_условие )
{
// ...
возвращаемое_значение = НЕЧТО;
goto выход;
}
else
{ // ...
возвращаемое_значение = НЕЧТО_ЕЩЕ;
goto выход;
}
exit:
return возвращаемое_значение;
}
Этот метод не срабатывает в С++, потому что функции конструктора вызываются неявно в качестве части объявления; объявление часто скрывает вызов функции. Если вы пропускаете объявление, то вы пропускаете и вызов конструктора. Например, в следующей программе деструктор для x вызовется, а конструктор нет:
foo()
{
if ( некое_условие )
goto выход;
некий_класс x; // Конструктор не вызывается. (Оператор goto
// перескакивает через него.)
// ...
выход:
// Здесь вызывается деструктор для x при выходе x из
} // области видимости.
Вследствие этой проблемы лучше всего совсем избегать переходов goto в программах на С++.
54.1. Всегда предусматривайте возврат значения из блока внешнего уровня.
Иногда, когда подпрограммы короткие, не стоит стараться обеспечить единственную точку выхода. (По моему мнению, правило "избегай запутанности" перекрывает любое другое правило, с которыми оно входит в конфликт). В этой ситуации всегда старайтесь убедиться, что из подпрограммы нет таких путей, которые не проходят через оператор return. Не так:
if ( a )
{
// ...
return делай_что_нужно();
}
else
{
// ...
return ОШИБКА;
}
а так:
if ( a )
{
// ...
return делай_что_нужно();
}
// ...
return ОШИБКА;
В идеале, выход по ошибке организуется из внешнего уровня блока так, чтобы вы правильно обработали неожиданный аварийный выход на внешний уровень.
55. Избегайте дублирования усилий.
Следующий фрагмент демонстрирует эту проблему:
if ( strcmp(a, b) < 0 )
{
}
else if ( strcmp(a, b) > 0 )
{
}
else if ( strcmp(a, b) == 0 )
{
}
Вызов strcmp() в С связан с немалыми накладными расходами (как в Паскале и других языках программирования). Вам значительно лучше сделать так:
int cmp = strcmp(a, b);
if ( cmp < 0 )
{
}
else if ( cmp > 0 )
{
}
else // остается случай cmp == 0
{
}
56. Не портьте область глобальных имен.
Порча области глобальных имен является характерной проблемой в среде групповой разработки. Вам не очень понравится спрашивать разрешение от каждого участника группы каждый раз, когда вы вводите новый идентификатор. Поэтому:
-
Локальная переменная всегда более предпочтительна, чем член класса.
-
Член класса всегда более предпочтителен, чем статическая глобальная переменная.
-
Статическая глобальная переменная всегда более предпочтительна, чем настоящая глобальная переменная.
Статический глобальный идентификатор не экспортируется из файла .c, поэтому он невидим из других модулей. Применяйте модификатор static к как можно большему числу глобальных идентификаторов (переменным и функциям). Ключ доступа private в определении класса еще лучше. Идентификатор, определенный локально внутри подпрограммы, лучше всех, потому что он изолирован от всех других функций в программе.
Вывод: избегайте препроцессора. Так как у макроса такая большая область видимости, то он по сути то же самое, что и глобальная переменная.
Характеристики
Тип файла документ
Документы такого типа открываются такими программами, как Microsoft Office Word на компьютерах Windows, Apple Pages на компьютерах Mac, Open Office - бесплатная альтернатива на различных платформах, в том числе Linux. Наиболее простым и современным решением будут Google документы, так как открываются онлайн без скачивания прямо в браузере на любой платформе. Существуют российские качественные аналоги, например от Яндекса.
Будьте внимательны на мобильных устройствах, так как там используются упрощённый функционал даже в официальном приложении от Microsoft, поэтому для просмотра скачивайте PDF-версию. А если нужно редактировать файл, то используйте оригинальный файл.
Файлы такого типа обычно разбиты на страницы, а текст может быть форматированным (жирный, курсив, выбор шрифта, таблицы и т.п.), а также в него можно добавлять изображения. Формат идеально подходит для рефератов, докладов и РПЗ курсовых проектов, которые необходимо распечатать. Кстати перед печатью также сохраняйте файл в PDF, так как принтер может начудить со шрифтами.














