Переменные: грамотное использование
Магия не в самих вещах, а в отношениях между обыкновенными вещами.
(с) Сальвадор Дали
Сложно найти программу, в которой не использовались бы переменные. Они являются одними из самых распространённых элементов исходного кода. Правильное их использование повышает степень качества кода, однако зачастую к переменным относятся легкомысленно, а зря. Сегодня мы рассмотрим основные принципы грамотного использования переменных.
Статья будет полезна не только начинающим программистам, но возможно поможет и более опытным расставить некоторые точки над i.
В этой статье мы под переменными будем понимать как низкоуровневые данные (int, double, и пр.), так и объекты из ООП.
Сила имён переменных
Выбор правильного имени для переменной в значительной мере обеспечивает читаемость кода и снижает вероятность появления ошибок, вызванных неправильным пониманием сути переменной со стороны другого разработчика или автора, сопровождающего программу спустя некоторое время.
Общие принципы выбора имён переменных
Выбирайте имена переменных со всей тщательностью. Хорошее имя должно полностью характеризовать суть переменной, легко читаться и запоминаться. Не жалейте для этих целей букв.
Используйте короткие имена только для локальных переменных, находящихся в малой области видимости. Например, имя i может отлично подойти для параметра цикла, но никак не годится для использования в качестве имени аргумента функции, данных класса или глобальной переменной (глобальные переменные стоит вообще избегать всеми возможными способами и использовать лишь в крайних случаях, это мы рассмотрим ниже).
Важнейший принцип именования переменных состоит в том, что имя должно полностью и точно описывать сущность, представляемую переменной. Например, если Вы хотите создать переменную, в которой будет храниться текущая дата, то лучше выбрать имя currentDate нежели просто date. Для хранения числа строк на странице документа лучше использовать имя linesPerPage, а не lines, lpp, n или вовсе x (последний вариант является наихудшим из представленных в данном случае).
Также не рекомендуется использовать слишком длинные имена (20 символов и выше). Слишком короткие имена тоже являются крайностью.
Немного о префиксах и постфиксах
Особые переменные наделяйте префиксами или постфиксами, отражающими их суть. Если Вам пришлось использовать глобальные переменные, можете добавлять в начало имени префикс G_, например G_machineStatus. Для переменных-данных класса можно использовать префикс _ (подчёркивание), например: hashsMap_ вместо простого hashsMap.
Префиксы и постфиксы позволяют подчеркнуть особый статус переменной, снижая вероятность ошибок. Так например при использовании глобальных переменных необходимо помнить о возможности побочных ошибок (когда переменная изменяется в разных местах кода и разработчик забывает это учесть, а также в многопоточном коде и других ситуациях).
Дисциплинированно используйте антонимы
Часто в коде встречаются антонимы, такие как begin/end, first/last, up/down.
Имена таких переменных должны быть симметричны, то есть следует использовать min/max, а не min/maximum. Кроме того, желательно определить такие пары и не изменять их составляющие в прочих ситуациях, так как это может привести к ошибкам человеческого фактора.
Именование индексов циклов
В простом цикле допустимо применять индекс i. Однако, во вложенных циклах такие имена могут привести к малозаметным, но критически важным ошибкам, например в коде C/C++:
Здесь ошибка во втором цикле: вместо инкрементирования переменной j происходит инкрементирование переменной i. Кроме того, подобная ошибка может быть допущена при использовании таких индексов: Matrix[i][j] или Matrix[j][i] ? Первая ошибка заметна: она приведёт к бесконечному выполнению вложенного цикла. Ошибка же в порядке индексов матрицы является более скрытой. При выполнении она может привести к использованию областей памяти, «чужих» по отношению к массиву (особенно когда ширина и высота матрицы различны). Последствия — непредсказуемы (и не всегда очевидны), а их причину сложно будет найти в большом приложении.
Поэтому во вложенных массивах рекомендуется использовать более осмысленные имена для переменных, например:
Да, размер кода повышается. Однако вместе с этим повышается его понятность и читабельность, а вероятность неправильного использования переменных — снижается.
Избегайте использования «магических чисел»
Часто можно встретить код, подобный:
if (printFlag == 0x10) //…
Здесь вместо «магического» шестнадцатеричного значения использовать именованную константу:
const unsigned int PrintSuccess = 0x10;
//…много кода…
if (printFlag == PrintSuccess) //…
Это не только повышает понятность кода, но и при возможном изменении значения для такого символа в будущем позволит изменить лишь определение константы, вместо выискивания и исправления по всему проекту значения 0x10 (если оно использовалось ещё и для других целей, то это ещё более повысит сложность такого исправления и может привести к ошибкам из-за человеческого фактора).
О булевых переменных
Присваивайте булевым переменным имена, подразумевающие true или false. Так, имена done и success — хорошие. Имя же status — плохо подходит для булевой переменной, так как не наделяет её значения смыслом. Вместо status лучше использовать например statusOk. Имя такой переменной позволяет быстро понять её назначение без обращения к документации проекта. Так, код условия
if (statusOk) //…
намного понятнее, чем
if (status) //…
Форматируйте имена так, чтобы они легко читались
Лучше использовать имя formattedPrint вместо frmttdPrnt. Использование удобочитаемых имён позволяет легче их запомнить, снижает количество потенциальных ошибок в именовании. Кроме того, сокращённые имена могут привести к появлению «дополнительных» переменных с ошибочным названием в добавок к имеющимся, что приведёт к ошибкам.
Используйте конвенции именования, чтобы избежать подобных ошибок, а также для того, чтобы код не был «разношёрстным», то есть был выполнен в едином стиле.
Также, в качестве аббревиатур использовать только общеизвестные. При использовании собственных аббревиатур указывайте их в спецификации проекта и старайтесь не злоупотреблять количеством таких названий, иначе код может стать сложным для понимания и сопровождения.
Сокращения допускаются, но с ними следует быть начеку: не допускайте потенциального изменения смысла имён вследствие таких манипуляций.
Имена, которых следует избегать
При именовании переменных избегайте:
— имён, которые являются неоднозначными
— применения нескольких имён, имеющих похожие значения
— переменных, имеющих похожие имена, но разную суть
— имён, имеющих похожее звучание, но разное написание
— имён, включающих цифры (за исключением случаев когда цифра относится к сути имени, например adler32code)
— смешения естественных языков
— имён стандартных типов, переменных и методов (это вызывает неоднозначность при чтении, а также может вызвать конфликт имён или скрытые логические ошибки)
— имён, которые не связаны с тем, что представляют
— имён, содержащих символы, которые можно спутать с другими символами (к таким символам относятся: 1 и l, 1 и I, O и 0, 2 и Z, S и 5, G и 6)
Не используйте имён, которые вводят в заблуждение.
Выбор хороших имён — одно из главных условий понятности программы.
Основные типы данных
Здесь Вы найдёте основные рекомендации по использованию переменных, имеющих основные типы. К таким типам относятся числа (целые и с плавающей точкой), символы, строки, логические переменные, перечисления, массивы.
Работа с числами
Как было указано выше, старайтесь избегать «магических чисел» — заменяйте их на именованные константы.
Избегайте ошибок деления на ноль — выполняйте проверки значений и обработку таких ситуаций.
Выполняйте преобразования типов очевидным образом и избегайте сравнения переменных разных типов. Для подобных сравнений лучше явно привести переменные к одному типу и только после этого сравнивать.
Обращайте внимание на предупреждения компилятора и грамотно исправляйте код, чтобы их избежать. Зачастую компиляторы предупреждают о сравнении переменных разных типов, например int cо float. Можно встретить код, подобный:
int maxValue = 5;
float currentValue = 5.5;
if (maxValue < currentValue) //…
Что имел ввиду автор?
Что maxValue < int(currentValue) или же float(maxValue) < currentValue ? Компилятор решит эту проблему однозначно, но совпадёт ли это решение с намерением автора? Поэтому следует явно преобразовывать тип в таких проектах к тому, который требуется чтобы избежать неоднозначности.
Целые числа
Проверяйте целочисленность операций деления. Когда целое число 6 делится на целое число 10, то, как правило, во многих языках программирования результатом будет не 0,6, а целое число 0. Если требуется результат с плавающей точкой, то необходимо привести операнды к соответствующему формату.
Проверяйте переполнение целых чисел. Если вы перемножаете два целых беззнаковых числа 250 и 300, то результатом будет 75000. Но не в случае если Вы используете 16-битовую переменную — в таком случае результат будет равен (75000 — 65536) = 9464 — вследствие переполнения значения. У переменных для каждой разрядности есть свой интервал возможных значений. Самым большим интервалом (среди стандартных типов данных) обладают 64-битные переменные (такие как long long int в C/C++ на многих, но не на всех (!) платформах).
Числа с плавающей точкой
Избегайте сложения и вычитания слишком разных по размеру чисел — это приводит к снижению точности результата. Объясняется это тем, что разрядность хранимого значения ограничена. Так, при сложении большого количества чисел из разных диапазонов значений, следует начинать с меньших чисел и заканчивать большими.
Старайтесь избегать сравнений на равенство чисел с плавающей точкой. Вместо этого лучше использовать специальную функцию, сравнивающую значения с некоторой точностью. Это обусловлено тем, что из-за конечной разрядности чисел результат обработки некоторого математического выражения может отличаться от ожидаемого. Так, при сложении 10 чисел 0,1 (типа float) нет никаких гарантий, что результат будет равен 1,0. Вполне вероятно, что вычисленный результат будет равен 0,99999999… и при сравнении такого числа на равенство с 1,0 результат будет отрицательным.
Учитывайте ошибки округления и по возможности избегайте их (можно использовать двоично-десятичное представление или числа с фиксированной точкой, которые можно эмулировать при помощи манипуляций с целыми числами).
Не рекомендуется использовать числа с плавающей точкой в качестве параметра цикла for и в подобных ситуациях. При значительной величине числа прибавление единицы перестаёт изменять значение такой переменной (из-за конечной точности). Если в такую ситуацию попадёт параметр цикла, то цикл станет бесконечным.
Символы и строки
Избегайте «магических» символов и строк. Так, вместо
if (inputChar == 0x20) //…
лучше использовать:
if (inputChar == Space) //…
где Space — именованная константа, равная 0x20.
При использовании строк в С-стиле следует помнить о наличии граничного нуля. Во избежание появления строк «бесконечной длины» инициализируйте их нулём при динамическом создании.
В языке C++ имеется специальный класс std::string, упрощающий работу со строками. Он берёт на себя управление памятью, а также предоставляет удобный синтаксис для работы со строковыми объектами.
Логические переменные
Используйте логические переменные для упрощения сложных условий if. Кроме того, хорошие имена таких переменных позволяют документировать код.
В языках, не имеющих определения логического типа, его можно определить или эмулировать. Так, в C его можно определить следующим образом:
typedef int bool;
const bool true = 1;
const bool false = 0;
Или через #define:
#define bool int
#define true 1
#define false 0
Перечислимые типы
Используйте перечислимые типы для надёжности. Так, вместо целочисленной переменной, значение которой означает одно из нескольких возможных состояний системы лучше использовать перечисление (enum).
Проверяйте некорректные значения. Так, в операторах switch/case следует проверять, не получено ли недопустимое значение переменной — используя ветку default.
C++ начиная с версии 2011 года (C++11) позволяет создавать перечисления в качестве отдельного типа данных, константы которого остаются в его области видимости:
enum class enumName {…};
Также, C++11 и старше позволяет определить целочисленный тип, который будет использоваться для внутреннего представления значений (может требоваться для удобства сохранения значения в файл или оптимизации памяти). Это делается следующим образом:
enum class: type {…};
где type — встроенный целочисленный тип. В качестве него можно использовать например псевдоним uint8_t, означающий беззнаковое 8-разрядное целое (обычно реализуется в стандартной библиотеке как unsigned char).
Массивы
Убедитесь, что значения индексов не выходят за пределы массива.
В высокоуровневых языках старайтесь использовать стандартные контейнеры вместо массивов.
В многомерных массивах убедитесь, что их индексы используются в правильном порядке
Указатели
Работать с указателями нужно с большой внимательностью. Не забывайте освобождать выделенную память когда она перестаёт быть необходимой. Не разыменовывайте указатели, данные которых были освобождены. Желательно, после освобождения указателя приравнивать его нулю. На языке C++11 и поздних версий для нулевого указателя есть ключевое слово nullptr. Лучше использовать его вместо NULL или 0 — не только с точки зрения стилистики, но и из-за того, что на некоторых платформах нулевой указатель может иметь ненулевое значение, а адрес 0x00 может означать вполне используемую ячейку памяти.
Заключение
Переменные являются основными составляющими большинства проектов. Их грамотное именование и использование позволяет значительно сократить вероятность допущения ошибок.
