Глобальные переменные (снова) - PullRequest
2 голосов
/ 05 ноября 2010

Я постоянно слышу, что глобальные переменные должны никогда не использоваться , но я склонен отклонять правила " никогда " какгоряч.Неужели нет никаких исключений?
Например, я сейчас пишу небольшую игру на c ++ с SDL.Мне кажется, имеет смысл иметь глобальную переменную с указателем на экранный буфер, потому что все разные классы, представляющие разные типы вещей в игре, должны будут к ней прибегнуть, и есть только одинэкранный буфер.

Пожалуйста, скажите мне, если я прав, что есть исключения, или если нет, то:

  • Почему бы не , или что в них такого плохого, что ониследует избегать любой ценой (пожалуйста, объясните немного)
  • Как этого достичь , желательно без необходимости передавать его каждому конструктору для внутреннего хранениядо необходимости или при каждом вызове запроса paint ().

(я мог бы предположить, что этот вопрос задавался ранее в SO, однако не смог найти то, что мне нужно (объяснение и обходной путь)) при поиске. Если кто-то может просто опубликовать ссылку на предыдущий вопрос, это может быть здорово)

Ответы [ 11 ]

8 голосов
/ 05 ноября 2010

Мы говорим студентам никогда не использовать глобальные переменные, потому что это поощряет лучшие методы программирования. По той же причине мы говорим им не использовать оператор goto. Как только вы станете опытным программистом, вы можете нарушать правила, потому что вы должны знать, когда это уместно.

7 голосов
/ 05 ноября 2010

Конечно, есть исключения. Лично я не могу вспомнить ни одной ситуации, когда goto - это правильное решение (или где singleton - правильное решение), но глобальные переменные иногда имеют свое применение. Но ... вы не нашли веского оправдания.

Большинство объектов в вашей игре не , повторяю, не требуется для доступа к экранному буферу. Это ответственность рендерера и никого больше. Вы не хотите, чтобы ваш логгер, менеджер ввода, ИИ или кто-то еще выводил на экран случайный мусор.

И именно поэтому люди говорят "не используйте глобалы". Это не потому, что глобалы - это какое-то абсолютное зло, а потому, что если мы не скажем это, люди попадут в ловушку, в которой вы находитесь, "да, но это правило не относится к me , верно? Мне нужно все, чтобы иметь доступ к X ". Нет, вам нужно научиться структурировать вашу программу.

Более распространенные исключения - для объектов без состояния или статических, таких как регистратор или, возможно, конфигурация вашего приложения: вещи, которые доступны только для чтения или только для записи и которые действительно должны быть доступны из везде . В каждой строке кода может потребоваться написать сообщение журнала. Таким образом, регистратор является честным кандидатом на то, чтобы стать глобальным. Но 99% вашего кода даже не должны знать, что экранный буфер существует .

Проблема глобалов заключается в том, что они нарушают инкапсуляцию: Код, который зависит от глобального, менее пригоден для повторного использования. Я могу взять тот же класс, который вы используете, поместить его в мое приложение, и он сломается. Потому что у меня нет той же сети глобальных объектов, от которой она зависит.

Это также делает код сложнее рассуждать. Какое значение вернет функция f(x)? Очевидно, это зависит от того, что x. Но если я дважды пройду один и тот же x, получу ли я такой же результат? Если он использует много глобалов, то, вероятно, нет. Тогда становится действительно трудно просто понять, что он собирается вернуть, а также то, что еще он собирается сделать. Будет ли она устанавливать какую-то глобальную переменную, которая будет влиять на другие, казалось бы, не связанные функции?

Как этого достичь, желательно без необходимости передавать его каждому конструктору для внутреннего хранения до необходимости

Ты говоришь так, будто это плохо. Если объект должен знать о экранном буфере, то вы должны дать ему экранный буфер. Либо в конструкторе, либо в более позднем вызове. (И у него есть приятный бонус: он предупреждает вас , если ваш дизайн неаккуратный. Если у вас есть 500 классов, которым нужно использовать экранный буфер, тогда вы должны передать его 500 конструкторам. Это больно, и так что это будильник: Я делаю что-то не так. Многим объектам не нужно знать о экранном буфере. Как я могу это исправить? `)

В качестве более очевидного примера, скажем, я хочу вычислить косинус 1.42, поэтому я передаю 1.42 функции: cos(1.42)

Вот как мы обычно это делаем, без глобалов. Конечно, мы могли бы вместо этого сказать: «Да, но каждый должен иметь возможность установить аргумент на cos, я бы лучше сделал его глобальным». Тогда это будет выглядеть так:

gVal = 1.42;
cos();

Я не знаю о вас, но я думаю, что первая версия была более читабельной.

7 голосов
/ 05 ноября 2010

Как и любое другое дизайнерское решение, использование глобальных переменных имеет свою стоимость.Это избавляет вас от необходимости передавать переменные без необходимости и позволяет разделять состояние между запущенными функциями.Но он также может затруднить отслеживание вашего кода и его повторное использование.

Некоторые приложения, например встроенные системы, регулярно используют глобальные переменные.Для них добавленная скорость отсутствия необходимости передавать переменную или даже указатель в запись активации и простота делают это хорошим решением [возможно].Но их код страдает от этого;часто трудно следить за выполнением, а разработка систем с возрастающей сложностью становится все более и более сложной.

В большой системе, состоящей из разнородных компонентов, использование глобальных переменных может стать кошмаром для обслуживания.В какой-то момент вам может понадобиться другой экранный буфер с другими свойствами, или экранный буфер может быть недоступен до тех пор, пока он не будет инициализирован, то есть вам придется обернуть каждый вызов к нему проверкой, если он нулевой, или вам нужно будет написатьмногопоточный код, и глобальный потребует блокировки.

Короче говоря, вы можете свободно использовать глобальные переменные, пока ваше приложение достаточно маленькое для управления.Когда он начнет расти, они станут пассивом и потребуют либо рефакторинга для удаления, либо ограничат рост программ (с точки зрения возможностей или стабильности).Предостережение не использовать их вытекает из многолетних усердных уроков, а не программистской «горячих голов».

3 голосов
/ 05 ноября 2010

Что если вы хотите обновить свой движок для поддержки двойного экрана? Несколько дисплеев становятся все более распространенными все время. Или что, если вы хотите ввести многопоточность? Взрыва. Как насчет того, если вы хотите поддерживать более одной подсистемы рендеринга? Whoopsie. Я хочу упаковать свой код как библиотеку для повторного использования другими людьми или мной? Дерьмо.

Другая проблема заключается в том, что порядок глобального init между исходными файлами не определен, что затрудняет поддержание более пары.

В конечном счете, у вас должен быть один и только один объект, который может работать с экранным буфером - объект рендеринга. Таким образом, указатель экранного буфера должен быть частью этого объекта.

Я согласен с вами с фундаментальной точки зрения - «никогда» неточно. Каждый вызов функции вызывает глобальную переменную - адрес этой функции. Это особенно верно для импортированных функций, таких как функции ОС. Есть и другие вещи, которые вы просто не можете разгадать, даже если бы захотели - например, куча. Тем не менее, это, безусловно, не то место, где можно использовать глобальный.

Самая большая проблема с глобальными именами заключается в том, что если позже вы решите, что глобальный вариант не подходит по какой-либо причине (и для этого есть много причин), то он абсолютно адский фактор в существующей программе. Простой факт заключается в том, что использование глобального просто не мышление. Я не могу потрудиться спроектировать реальную подсистему и объект рендеринга, поэтому я просто собираюсь разбросать эту штуку в глобальном масштабе. Это легко, просто, и не делать этого было самой большой революцией в программировании, когда-либо и по уважительной причине.

Создать класс рендеринга. Поместите указатель туда. Используйте функцию-член. Проблема решена.

Редактировать: я перечитал ваш ОП. Проблема в том, что вы разделили свои обязанности. Каждый класс (растровое изображение, текст, что угодно) НЕ должен отображаться сам. Он должен просто содержать данные, необходимые для визуализации главного объекта рендеринга. Работа растрового изображения - представлять растровое изображение, а не отображать растровое изображение.

2 голосов
/ 05 ноября 2010

Если экранный буфер распределяется между множеством различных фрагментов кода, у вас есть два варианта:

1) Обойти его повсюду.Это неудобно, потому что каждый фрагмент кода, который использует экранный буфер, даже косвенно, должен быть кропотливо обозначен как факт тем, что этот объект передается через стек вызовов.

2) Используйте глобальный.Если вы сделаете это, то, насколько вам известно, любая функция во всей вашей программе может использовать экранный буфер, просто извлекая его из глобального [*].Так что если вам нужно рассуждать о состоянии экранного буфера, то вам нужно включить всю программу в свои рассуждения.Если бы был какой-то способ указать, какие функции модифицируют экранный буфер, а какие нет.Ой, подождите секунду ...

Это даже помимо преимуществ внедрения зависимостей - при тестировании и в будущих итерациях вашей программы это может быть полезно для вызывающей функции некоторой функции, которая бьёт,чтобы иметь возможность сказать , где он должен мигать, а не обязательно на экране.

Те же проблемы применимы как к синглетам, так и к другим изменяемым глобальным переменным.

Возможно, вы даже могли бы доказать, что должно стоить вам чего-то, чтобы добавить еще один фрагмент кода, который модифицирует экранный буфер, потому что вы должны попытаться написать системы, которые слабо связаны,и, естественно, это приведет к тому, что для выполнения своей работы будет довольно мало фрагментов кода, которым нужно что-либо вообще знать об экране (даже если они знают, что манипулируют изображениями, им не обязательно заботиться о том, являются ли эти изображенияв экранном буфере, или в некотором заднем буфере, или в каком-то совершенно не связанном буфере, который не имеет ничего общего с экраном).На самом деле я не за то, чтобы выполнять дополнительную работу, чтобы наказать себя за написание лучшего кода, но, безусловно, верно, что глобальные переменные упрощают добавление еще одной неуместной связки в мое приложение.

[*Ну, вы можете сузить его на том основании, что декларация будет иметь только TU, которые включают соответствующий заголовочный файл.Технически ничто не мешает им копировать и вставлять это, но в кодовой базе, которая вообще хорошо отрегулирована, они не будут.

2 голосов
/ 05 ноября 2010
  • Глобальные переменные могут изменяться неожиданным образом, что обычно не то, что вам нужно.Состояние приложения станет сложным и не поддерживаемым.Очень легко сделать что-то не так.Особенно, если кто-то еще меняет ваш код;

  • Синглтон может быть лучшей идеей.Это, по крайней мере, даст вам некоторую инкапсуляцию на случай, если вам понадобится сделать расширения в будущем.

  • Одной из причин не использовать глобальные переменные является проблема с пространствами имен (то есть случайное использование того же именидважды);

  • Мы часто используем глобальные (для пространства имен) константы в работе, которые считаются нормальными, поскольку они не изменяются (неожиданными способами), и это очень удобно иметьони доступны в нескольких файлах.

1 голос
/ 05 ноября 2010

«Почему бы и нет»: глобальные переменные дают вам поток информации для спагетти.

Это то же самое, что goto дает вам поток управления для спагетти.

Вы не знаете, откуда что-то происходит, иличто можно предположить в любой момент.Решение INTERCAL о введении оператора come from, хотя и давало некоторую первоначальную надежду окончательно убедиться в том, откуда взялся контроль, оказалось, что на самом деле не решило эту проблему для goto.Точно так же более современные языковые функции для отслеживания обновлений глобальных переменных, такие как onchangeby, не смогли решить эту проблему для глобальных переменных.

Cheers & hth.,

1 голос
/ 05 ноября 2010

Причина, по которой я никогда этого не делаю, заключается в том, что это создает беспорядок.Представьте, что для ВСЕХ уникальных переменных заданы глобальные переменные, у вас будет внешний список размером с телефонную книгу.

Другая причина может заключаться в том, что вы не знаете, где она инициализируется или изменяется.Что если вы случайно измените его в месте X в файле Y?Ты никогда не узнаеешь.Что если он еще не инициализирован?Вам придется проверять каждый раз.

if (global_var = 0) // uh oh :-(
if (object->Instance() = 0) // compile error :-)

Это можно исправить с помощью синглетонов.Вы просто не можете назначить функцию, возвращающую вам адрес объекта.

Кроме того: вам не нужен ваш экранный буфер везде в вашем приложении, однако, если вы хотите: продолжайте, это не делаетпрограмма работает не так хорошо: -)

И тогда у вас все еще есть проблема с пространством имен, но это по крайней мере дает вам ошибки компиляции; -)

0 голосов
/ 05 ноября 2010

В этом случае класс, который предоставляет функции-члены для всех методов, которым требуется доступ к экранному буферу, был бы более дружественным подходом ООП.Почему все и каждый должны иметь неконтролируемый доступ к нему!?

Относительно того, бывают ли времена, когда глобальное лучше или даже необходимо, вероятно, нет.Они обманчиво привлекательны, когда вы взламываете какой-то код, потому что вам не нужно прыгать через синтаксические обходы, чтобы получить к нему доступ, но это, как правило, указывает на плохой дизайн, и тот, который быстро атрофирует межоперационное обслуживание и расширение.

Вот хорошее прочтение по теме (относится к встроенному программированию, но пункты применимы к любому коду, просто некоторые встроенные программисты считают, что у них есть веское оправдание).

0 голосов
/ 05 ноября 2010

Глобальные переменные (и синглеты, которые являются просто оберткой вокруг глобальной переменной) могут вызвать ряд проблем, как я обсуждал в этот ответ .

Для этой специфической проблемы - блитируемых объектов в игровом наборе - я был бы склонен предложить сигнатуру метода, такую ​​как Sprite::drawOn(Canvas&, const Point&). Передача ссылки на Canvas не должна быть чрезмерной, так как она вряд ли понадобится, кроме как в пути рисования, и внутри этого пути вы, вероятно, все равно перебираете коллекцию, поэтому передача ее в этот цикл не не так сложно. Делая это, вы скрываете, что основная программа имеет только один активный экранный буфер из классов спрайтов, и, следовательно, снижаете вероятность создания зависимости от этого факта.

Отказ от ответственности: я сам раньше не использовал SDL, но я написал простой кроссплатформенный игровой набор C ++ еще в конце 90-х. В то время, когда я работал над своим игровым комплектом, для многопользовательских игр на основе X11 было довольно распространенной практикой запускаться как один процесс на одной машине, который открывал соединение с дисплеем каждого игрока, что довольно эффективно создавало беспорядок. код, который предполагал, что буфер экрана был синглтоном.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...