告别全局变量!

在 Lua 中,全局变量和函数存在风险,在考虑创建/添加到 _G 表(Lua 的“全局表”)之前,您应该了解一些关键事项。(Lua 的“全局表”).

首先,让我们快速讨论一下 _G 表。在 Lua 中,所有大写字母并以下划线 (_) 开头的变量都被视为 Lua 使用的私有变量,例如 _VERSION。在任何情况下,您都不应尝试在程序中使用这些变量。考虑到这一点,请注意 _G 也是大写字母并以下划线开头!这意味着它是系统使用的保留结构。

全局变量的便利性

全局变量和函数确实很方便。通过使用它们,您可以创建一个可以从 main.lua 和任何其他 Lua 模块(例如 Composer 场景)访问的对象。但是,重要的是要理解以下两个变量完全相同:

myVariable = 10
_G.myVariable = 10

换句话说,Lua 将每个全局变量放入 _G 表中,并且您显式放入 _G 中的所有内容都可以通过其变量名访问。例如:

_G.myVariable = 20
print( myVariable )  --> Prints 20 to the console

那么,为什么要像上面的示例那样将全局变量显式放入 _G 表中呢?一个原因是明确标识您正在声明一个全局变量,而不是一个您打算声明为局部变量但只是忘记添加 local 前缀的变量。本质上,在全局声明前加上 _G. 可以清楚地表明您在返回检查/修改代码时的意图。

无害,无犯规?

一些开发者报告说,当他们使用 print() 将变量输出到控制台时,Solar2D 的错误和警告消息停止工作。可能的原因是开发者创建了一个名为 debug 的全局变量,即 _G.debug,这样做无意中“破坏”了对 Solar2D 用于输出调试消息的关键内部库的访问。

要进一步检查此概念,您可以使用以下方法 print() 整个 _G 表:

for k,v in pairs( _G ) do
    print( k .. " : ", v )
end

运行此代码将输出 _G 的全部内容,包括:

_G :        table: 0x610000468580
_VERSION :  Lua 5.1
package :   table: 0x600000870300
tostring :  function: 0x60000024d290
print :     function: 0x60000024c870
lpeg :      table: 0x610000463180
os :        table: 0x600000a6c240
unpack :    function: 0x60000024cf00
lfs :       table: 0x610000469e80
require :   function: 0x60000067af00
debug :     table: 0x600000270a80

API 参考(全局变量)条目下列出的所有 API,以及一些未记录的项目,如 debug 和几个核心 API 组,如 mathstringaudio。考虑到这一点,您可能会认为您可以编写如下代码:

_G.audio = true  -- Play audio if true, don't play if false
_G.type = "Gold"
_G.timer = timer.performWithDelay( 1000, doSomething )

但是,这样做只会“破坏”这些内部 Solar2D 库,这意味着您将无法播放音频或使用 type() 函数 — 并且计时器将无法计时!

您可能认为解决方案是简单地避免使用那些全局名称。别着急!上面循环输出的列表指示了您今天不应该覆盖的库,基于一个空的 main.lua 文件。该列表还假设您尚未:require()-d任何其他库或模块,无论是 Solar2D 库(如 physics)、您自己的模块,还是第三方库/插件。此外,其他一些您根本不知道的东西也可能会进入全局空间。

全局变量危险的另一个原因是,由于它们没有可见性限制,因此在性能方面使用成本可能很高。事实上,如果在时间紧迫的循环中访问全局变量,它的性能可能会比同一循环中的局部变量差 30%。

避免使用全局变量

一些开发者使用(有时是滥用)全局变量来访问尚未声明的内容,或在模块之间访问变量/函数。幸运的是,这两项任务都可以无需使用全局变量轻松完成!

前置声明

第一种情况(访问尚未声明的内容)可以通过前置声明方法解决。作为 Lua/Solar2D 程序员,了解作用域至关重要。如果您需要在声明变量(通常是函数)之前使用它,只需前置声明一个变量并使用备用函数声明语法,如下所示:

local doSomething  -- Forward declaration

local function makeSomethingHappen()
    local count = 10
    doSomething( count )
end

doSomething = function( count )  -- Assign function to forward declaration
    print( count )
end

makeSomethingHappen()

在第 8 行,我们只是将执行工作的函数分配给在第 1 行前置声明的变量 doSomething。这样,makeSomethingHappen() 函数(第 3-6 行)就会知道 doSomething() 函数,因为它是在请求它的代码块“上方”声明的。

要了解更多关于 Lua 作用域和遵守作用域规则的重要性,请参阅 初学者作用域 教程。

数据模块

要解决第二种情况(在模块之间访问变量),您可以简单地创建一个名为 globalData.lua(或类似名称)的 Lua 文件,其内容如下:

-- Pseudo-global space
local M = {}

return M

然后,在您的每个 Lua 模块中,只需按如下方式 require() 该模块:

local globalData = require( "globalData" )

之后,设置“全局”变量就像这样简单:

globalData.playerName = "John Smith"

并且从任何模块读取值(假设您包含了local globalData = require( "globalData" ))—就像这样简单:

local currentPlayer = globalData.playerName

如您所见,此方法允许您像使用 _G 一样使用 globalData 表,并避免覆盖 _G 表中内容的风险,以及避免全局变量带来的常见问题和陷阱。

总结

我们一直强调“避免全局变量!”的立场,正如您通过上述方法所看到的,基本上不再需要全局变量或函数。通过谨慎的作用域和使用伪全局模块,您无需全局变量即可完成所有需要完成的任务。