6.9 – Почему глобальные переменные – это зло
Если бы вы спросили опытного программиста совет по передовым методам программирования, после некоторого размышления наиболее вероятным ответом было бы: «Избегайте глобальных переменных!». И на то есть веская причина: глобальные переменные – одно из наиболее часто используемых понятий в языке. Хотя в небольших учебных программах они могут показаться безобидными, в более крупных они часто создают проблемы.
У начинающих программистов часто возникает соблазн использовать множество глобальных переменных, потому что с ними легко работать, особенно когда задействовано много вызовов различных функций (передача данных через параметры функций – это боль). Однако, как правило, это плохая идея. Многие разработчики считают, что следует полностью избегать использования неконстантных глобальных переменных!
Но прежде чем мы продолжим, необходимо уточнить, почему это так. Когда разработчики говорят вам, что глобальные переменные – это зло, они обычно говорят не обо всех глобальных переменных. В основном они говорят о неконстантных глобальных переменных.
Почему (неконстантные) глобальные переменные – зло
Безусловно, основная причина опасности неконстантных глобальных переменных заключается в том, что их значения могут быть изменены любой вызываемой функцией, и у программиста нет простого способа узнать о том, что это произошло. Рассмотрим следующую программу:
int g_mode; // объявляем глобальную переменную (по умолчанию будет инициализирована нулем)
void doSomething()
{
g_mode = 2; // устанавливаем глобальную переменную g_mode равной 2
}
int main()
{
g_mode = 1; // Примечание: это устанавливает глобальную переменную g_mode в 1.
// Тут не объявляется локальная переменная g_mode!
doSomething();
// Программист все еще ожидает, что g_mode будет равна 1
// Но doSomething изменил ее на 2!
if (g_mode == 1)
std::cout << "No threat detected.\n";
else
std::cout << "Launching nuclear missiles...\n";
return 0;
}
Обратите внимание, что программист установил для переменной g_mode
значение 1, а затем вызвал doSomething()
. Если программист явно не знал, что doSomething()
собирается изменить значение g_mode
, он или она, вероятно, не ожидали, что doSomething()
изменит это значение! Следовательно, остальная часть main()
работает не так, как ожидает программист (и мир уничтожается).
Короче говоря, глобальные переменные делают состояние программы непредсказуемым. Вызов каждой функции становится потенциально опасным, и у программиста нет простого способа узнать, какие из них опасны, а какие нет! Локальные переменные намного безопаснее, потому что другие функции не могут влиять на них напрямую.
Есть много других веских причин не использовать неконстантные глобальные переменные.
С глобальными переменными нередко можно найти фрагмент кода, который выглядит следующим образом:
void someFunction()
{
// полезный код
if (g_mode == 4) // сделать что-нибудь хорошее
}
После отладки вы определяете, что ваша программа работает некорректно, потому что g_mode
имеет значение 3, а не 4. Как это исправить? Теперь вам нужно найти все места, где для g_mode
может быть установлено значение 3, и, в первую очередь, проследить, как оно было установлено. Возможно, это может быть в совершенно не связанном фрагменте кода!
Одна из основных причин объявлять локальные переменные как можно ближе к тому месту, где они используются, заключается в том, что это сводит к минимуму объем кода, который необходимо просмотреть, чтобы понять, что делает переменная. Глобальные переменные находятся на противоположном конце спектра – поскольку к ним можно получить доступ где угодно, вам, возможно, придется просмотреть всю программу, чтобы понять их использование. В небольших программах это может не быть проблемой. В больших так оно и будет.
Например, вы можете обнаружить, что g_mode
упоминается в вашей программе 442 раза. Если g_mode
не очень хорошо документирована, вам, возможно, придется просмотреть каждое использование g_mode
, чтобы понять, как она используется в разных случаях, каковы ее допустимые значения и каково ее общее назначение.
Глобальные переменные также делают вашу программу менее модульной и менее гибкой. Функция, которая не использует ничего, кроме своих параметров и не имеет побочных эффектов, является идеально модульной. Модульность помогает как в понимании того, что делает программа, так и в возможности повторного использования кода. Глобальные переменные значительно снижают модульность.
В частности, избегайте использования глобальных переменных для переменных важных «точек принятия решений» (например, для переменных, которые вы использовали бы в условном выражении, например, для переменной g_mode
в приведенном выше примере). Ваша программа вряд ли сломается, если глобальная переменная, содержащая информационное значение (например, имя пользователя), изменится. Вероятность поломки гораздо выше, если вы измените глобальную переменную, которая влияет на то, как на самом деле работает ваша программа.
Лучшая практика
По возможности используйте локальные переменные вместо глобальных.
Итак, каковы веские причины использовать неконстантные глобальные переменные?
Их немного. В большинстве случаев существуют другие способы решения задачи, позволяющие избежать использования неконстантных глобальных переменных. Но в некоторых случаях разумное использование неконстантных глобальных переменных действительно может снизить сложность программы, и в этих редких случаях их использование может быть лучше, чем альтернативы.
Хорошим примером является лог-файл, в который можно записать информацию об ошибках или отладочную информацию. Вероятно, имеет смысл определить его как глобальный объект, потому что у вас в программе, вероятно, будет только один лог-файл, и он, вероятно, будет использоваться в вашей программе повсюду.
Как бы то ни было, объекты std::cout
и std::cin
реализованы как глобальные переменные (внутри пространства имен std
).
Как показывает практический опыт, любое использование глобальной переменной должно соответствовать, по крайней мере, следующим двум критериям: переменная в вашей программе всегда должна представлять только одну вещь, и ее использование должно быть повсеместным во всей программе.
Многие начинающие программисты ошибаются, думая, что что-то может быть реализовано как глобальное, потому что оно прямо сейчас необходимо в единственном экземпляре. Например, вы можете подумать, что, поскольку вы реализуете однопользовательскую игру, вам нужен только один игрок. Но что произойдет позже, когда вы захотите добавить многопользовательский режим?
Защита от глобального разрушения
Если вы найдете хорошее применение неконстантной глобальной переменной, несколько полезных советов сведут к минимуму количество проблем, с которыми вы можете столкнуться. Этот совет не только для неконстантных глобальных переменных, он может помочь со всеми глобальными переменными.
Во-первых, чтобы уменьшить вероятность конфликтов имен, ко всем глобальным переменным без пространства имен добавьте префикс "g" или "g_" или, еще лучше, поместите их в пространство имен (обсуждается в уроке «6.2 – Пользовательские пространства имен»).
Например, вместо:
constexpr double gravity { 9.8 }; // из имени неясно, локальная это переменная или глобальная
int main()
{
return 0;
}
Сделайте так:
namespace constants
{
constexpr double gravity { 9.8 };
}
int main()
{
return 0;
}
Во-вторых, вместо прямого доступа к глобальной переменной лучше ее «инкапсулировать». Во-первых, убедитесь, что к переменной можно получить доступ только из файла, в котором она объявлена, например, сделав переменную статической или константной. Во-вторых, предоставьте внешние глобальные «функции доступа» для работы с переменной. Эти функции могут гарантировать правильное использование (например, проверка ввода, проверка диапазона и т.д.). Кроме того, если вы когда-нибудь решите изменить базовую реализацию (например, перейти из одной базы данных в другую), вам нужно будет обновить только функции доступа, а не каждый фрагмент кода, который напрямую использует глобальную переменную.
Например, вместо:
namespace constants
{
extern const double gravity { 9.8 }; // имеет внешнее связывание,
// напрямую доступна другим файлам
}
Сделайте так:
namespace constants
{
const double gravity { 9.8 }; // имеет внутреннее связывание,
// доступна только из этого файла
}
// эту функцию можно экспортировать в другие файлы для доступа
// к глобальной переменной за пределами этого файла
double getGravity()
{
// Мы можем позже добавить сюда логику, если понадобится
// или изменить реализацию прозрачно для вызывающих
return constants::gravity;
}
Напоминание
По умолчанию переменные const
имеют внутреннее связывание, gravity
не обязательно должна быть static
.
В-третьих, при написании автономной функции, использующей глобальную переменную, не используйте ее непосредственно в теле функции. Вместо этого передайте ее как аргумент. Таким образом, если вашей функции когда-либо в каких-то обстоятельствах понадобится использовать другое значение, вы можете просто изменить аргумент. Это поможет сохранить модульность.
Вместо:
#include <iostream>
namespace constants
{
constexpr double gravity { 9.8 };
}
// Эта функция полезна для вычисления вашей мгновенной скорости
// только на основе глобальной переменной gravity
double instantVelocity(int time)
{
return constants::gravity * time;
}
int main()
{
std::cout << instantVelocity(5);
}
Сделайте так:
#include <iostream>
namespace constants
{
constexpr double gravity { 9.8 };
}
// Эта функция может вычислить мгновенную скорость
// для любого значения gravity (более полезно)
double instantVelocity(int time, double gravity)
{
return gravity * time;
}
int main()
{
// передаем нашу константу в функцию как параметр
std::cout << instantVelocity(5, constants::gravity);
}
Шутка
Какой префикс лучше всего подходит для имени глобальной переменной?
Ответ: //