我一直听说全局变量应该 从不 ,但我倾向于忽略“ 从不 “规则头脑冷静。真的没有例外吗?
例如,我目前正在用SDL用c ++编写一个小游戏。在我看来,有一个带有指向屏幕缓冲区的指针的全局变量很有意义,因为代表游戏中不同类型的东西的所有不同的类都需要对它进行blit,并且只有一个屏幕缓冲区。
请告诉我,如果我是对的,有异常,或者如果不是,那么:
(我会假设这个问题之前已被问过,但在搜索时找不到我需要的东西(解释和解决方法)。如果有人可以发布前一个问题的链接,那可能会很棒)
答案 0 :(得分:8)
我们告诉学生永远不要使用全局变量,因为它鼓励更好的编程方法。这与我们告诉他们不要使用goto语句的原因相同。一旦你成为一名有成就的程序员,你就可以违反规则,因为你应该知道什么时候合适。
答案 1 :(得分:7)
与任何其他设计决策一样,使用全局变量会产生成本。它可以节省您不必要地传递变量,并允许您在运行的函数之间共享状态。但它也有可能使您的代码难以遵循和重用。
某些应用程序(如嵌入式系统)会定期使用全局变量。对于他们来说,不必将变量甚至指针传递到激活记录中的附加速度和简单性使得它成为一个好的决定[可以说]。但是他们的代码受到了损害;通常很难跟上执行,开发复杂程度越来越高的系统变得越来越困难。
在一个由异构组件组成的大型系统中,使用全局变量可能会成为维护的噩梦。在某些时候,您可能需要一个具有不同属性的不同屏幕缓冲区,或者屏幕缓冲区可能无法使用,直到它初始化为止意味着您必须将每次调用都包装起来,并检查它是否为空,否则您需要编写多线程代码,全局需要锁定。
简而言之,当您的应用程序足够小以便管理时,您可以自由使用全局变量。当它开始增长时,它们将成为一种责任,并且要么需要重构才能消除,或者将削弱程序的增长(在能力或稳定性方面)。不使用它们的警告源于多年的艰苦学习课程,而不是程序员“头脑冷静”。
答案 2 :(得分:7)
当然也有例外。我个人无法想到一个单一的情况,即goto是正确的解决方案(或单例是正确的解决方案),但全局变量偶尔会有其用途。但是......你没有找到有效的借口。
游戏中的大多数对象不,重复,不需要访问屏幕缓冲区。这是渲染器和没有人的责任。您不希望您的记录器,输入管理器,AI或任何其他人在屏幕上放置随机垃圾。
这就是人们说“不要使用全局”的原因。这不是因为全局变量是某种最终的邪恶,而是因为如果我们不这样说,那么人们会陷入你所处的陷阱中,“是的但是这条规则并不适用于< em> me ,对吗?我需要一切都可以访问X“。不,你需要学习构建你的程序。
更常见的例外情况是无状态或静态对象,例如记录器,或者可能是应用程序的配置:只读或只写的内容,以及真正需要从所有位置访问的内容< / em>的。每行代码都可能需要编写日志消息。因此,记录器是制作全球的公平候选者。但99%的代码甚至不需要知道屏幕缓冲区存在。
简而言之,全局变量的问题是它们违反了封装: 依赖于全局的代码不太可重用。我可以使用你正在使用的完全相同的课程,把它放在我的应用程序中,它会破坏。因为我没有相同的全局对象网络。
这也使得代码更难以推理。函数f(x)
返回什么值?
这显然取决于x
是什么。但如果我两次传递相同的x
,我会得到相同的结果吗?如果它使用了很多全局变量,那么可能不会。然后很难弄清楚它将返回什么,以及它将要做什么 else 。它是否会设置一些会影响其他看似无关的函数的全局变量?
如何实现这一点,最好不必将其传递给每个内部存储的构造函数,直到需要
你觉得这听起来很糟糕。如果一个对象需要知道屏幕缓冲区,那么你应该给它屏幕缓冲区。在构造函数中,或在稍后的调用中。 (并且它有一个很好的奖励:如果你的设计很草率,它会警告你。如果你有500个类需要使用屏幕缓冲区,那么你必须将它传递给500个构造函数。这很痛苦,所以这是一个警钟:我做错了。很多对象不需要知道屏幕缓冲区。我该怎么解决这个问题?`)
作为一个更明显的例子,假设我想计算1.42的余弦,所以我将1.42传递给函数:cos(1.42)
这就是我们通常的做法,没有全局变量。当然,我们可以改为说“是的但是每个人都需要能够将论证设置为cos
,我最好把它变成全球性的”。然后它看起来像这样:
gVal = 1.42;
cos();
我不了解你,但我认为第一个版本更具可读性。
答案 3 :(得分:3)
如果您想更新引擎以支持双屏幕,该怎么办?多个显示器一直变得越来越普遍。或者如果你想引入线程怎么办?砰。如果你想支持多个渲染子系统怎么样? Whoopsie。我想将我的代码打包为其他人或我自己重用的库?垃圾。
另一个问题是源文件之间的全局初始化顺序是未定义的,这使得维护多个文件变得棘手。
最终,您应该只有一个可以使用屏幕缓冲区的对象 - 渲染对象。因此,屏幕缓冲区指针应该是该对象的一部分。
我从基本的角度同意你的观点 - “从不”是不准确的。您调用的每个函数调用都是调用一个全局变量 - 该函数的地址。对于OS功能等导入功能尤其如此。还有其他一些你根本无法全球化的东西,即使你想要 - 就像堆一样。但是,这绝对不是使用全局的正确位置。
全局变量的最大问题在于,如果你后来因为任何原因决定全局不是正确的事情(并且有很多原因),那么它们绝对不能分解现有的程序。一个简单的事实是,使用全局只是没有思考。我不能为设计一个真正的渲染子系统和对象而烦恼,所以我只是想把这些东西放在全球范围内。它很简单,很简单,而且这不是软件编程中最大的革命,无论如何,并且有充分的理由。
制作渲染类。把指针放在那里。使用成员函数。问题解决了。
编辑:我重读了你的OP。这里的问题是你分担了你的责任。每个类(位图,文本,等等)都不应该呈现自己。它应该只保存主渲染对象需要渲染的数据。这是Bitmap的一项工作,用于表示位图 - 而不是渲染位图。
答案 4 :(得分:2)
全局变量可能会以意想不到的方式发生变化,这通常不是您想要的。应用程序的状态将变得复杂且不可维护。很容易犯错。特别是如果其他人正在更改您的代码;
Singleton可能是个更好的主意。如果您将来需要进行扩展,至少会给您一些封装。
不使用全局变量的一个原因是名称空间存在问题(即意外地使用相同名称两次);
我们经常在工作中使用全局(命名空间)常量,这些常量被认为是正常的,因为它们不会发生变化(以意想不到的方式),并且在多个文件中提供它们非常方便。
答案 5 :(得分:2)
如果屏幕缓冲区在许多不同代码之间共享,那么您有两个选择:
1)将它传遍整个地方。这是不方便的,因为使用屏幕缓冲区的每一段代码,甚至是间接的,都需要通过调用堆栈传递这个对象来进行费力的指示。
2)使用全局。如果你这样做,那么对于所有你知道整个程序中的任何函数的人都可以使用屏幕缓冲区,只需从全局[*]中获取它。因此,如果您需要推断屏幕缓冲区的状态,那么您需要在推理中包含整个程序。如果只有某种方式来指示哪些函数修改了屏幕缓冲区,哪些函数不可能这样做。哦,等一下......
这甚至不包括依赖注入的好处 - 在测试时,以及在程序的未来迭代中,它可能对某些函数的调用者有用,可以说 where 它应该是blit,不一定是屏幕。
同样的问题同样适用于单身人士,也适用于其他可修改的全局内容。
你甚至可能会认为应该花费你一些东西来添加修改屏幕缓冲区的另一段代码,因为你应该尝试编写系统松散耦合,这样做自然会产生相当少的代码片段,需要知道关于屏幕的任何事情才能完成他们的工作(即使他们知道他们正在操纵图像,他们不一定需要关心这些图像是在屏幕缓冲区,还是一些后台缓冲区,或者与屏幕无关的一些完全不相关的缓冲区)。我实际上并不赞成做额外的工作只是为了惩罚自己编写更好的代码,但是全局变量很容易让我很容易添加另一种不适当的耦合到我的应用程序。
[*]嗯,你可以缩小范围,因为只有包含相关头文件的TU才会有声明。从技术上讲,没有什么可以阻止它们复制和粘贴它,但是在一个完全受到监管的代码库中,它们不会。
答案 6 :(得分:1)
我从不这样做的原因是因为它造成了一团糟。想象一下,将所有唯一变量设置为全局变量,您将拥有一个与电话簿大小相同的外部列表。
另一个原因可能是您不知道它在何处被初始化或修改。如果您意外地在文件Y中的位置X修改它会怎么样?你永远不会知道。如果还没有初始化怎么办?你必须每次检查。
if (global_var = 0) // uh oh :-(
if (object->Instance() = 0) // compile error :-)
这可以使用单身人士来修复。你只是不能指定一个函数来返回对象的地址。
除此之外:你的应用程序中不需要你的屏幕缓冲区,但是如果你想:继续,它不会使程序运行不太好: - )
然后你仍然有命名空间问题,但至少会给你编译错误; - )
答案 7 :(得分:1)
“为什么不”:全局变量为您提供意大利面信息流。
这与goto相同,为你提供意大利面控制流程。
您不知道任何来自何处,或者在任何时候都可以假设什么。引入come from
语句的INTERCAL解决方案,虽然提供了最终确定控制来自何处的初步希望,但结果并未真正解决goto
的问题。同样,用于跟踪全局变量更新的更现代的语言功能(如onchangeby
)还没有解决全局变量的问题。
干杯&amp;第h。,
答案 8 :(得分:0)
全局变量(和单例,它只是一个全局变量的包装)可能会导致许多问题,正如我在this answer中所讨论的那样。
对于此特定的问题 - 游戏工具包中的blittable对象 - 我倾向于建议像Sprite::drawOn(Canvas&, const Point&)
这样的方法签名。传递对Canvas的引用不应该是过多的开销,因为除了paint通道之外不太可能需要它,并且在那个通道中你无论如何都可能在一个集合上迭代,所以在那个循环中传递它不是那很难。通过这样做,您隐藏主程序只有一个来自精灵类的活动屏幕缓冲区,因此不太可能创建对此事实的依赖。
免责声明:我之前没有使用过SDL,但是我在90年代后期编写了一个简单的跨平台C ++游戏套件。当我正在开发我的游戏套件时,基于X11的多人游戏在一台机器上作为单个进程运行是相当普遍的做法,该机器打开了与每个玩家的显示器的连接,这将非常有效地混乱假设屏幕缓冲区为单例的代码。
答案 9 :(得分:0)
在这种情况下,为需要访问屏幕缓冲区的所有方法提供成员函数的类将是一种更友好的OOP方法。为什么每个人和任何人都无法控制访问它??
关于全球是否有更好或甚至是必要的时候,可能不是。当你修改一些代码时,它们具有欺骗性的吸引力,因为你需要跳过没有句法箍来访问它,但它通常表明设计不良,并且会迅速摧毁维护和扩展。
Here's a good read on the subject(与嵌入式编程相关,但这些要点适用于任何代码,只是一些嵌入式程序员认为他们有正当理由)。
答案 10 :(得分:-2)
我也很想听听确切的解释,但我可以告诉你,Singleton模式通常可以很好地填补全局变量的作用。