love2d-chapter9-multiple-files-and-scope

love2d-chapter9-multiple-files-and-scope

十一月 28, 2020

第九章-多文件和作用域

多文件

我们可以把代码分散在多个文件里来更好地组织它们。创建一个新文件并将其命名为example.lua,然后再创建一个main.lua,把它们放到同一个文件夹下。
example.lua里创建一个变量。在接下的示例代码的开头,我会加入额外的注释行(--! file:)来明确这些代码应该放在哪个文件内。如果没有特别指出,那么这段代码要么是在main.lua里,要么是在之前提及的某个文件中。

1
2
--! file: example.lua
test = 20

然后在main.lua里输入print(test)。现在当你运行游戏,你因该看到输出test的结果是nil而不是20。这是因为我们必须先加载example.lua这个文件。要加载一个文件,我们使用require

1
2
3
4
5
--! file: main.lua
--Leave out the .lua
-- No need for love.load or whatever
require("example")
print(test)

我们不需要加上文件的拓展名“.lua”。
被加载的文件也可以在一个子目录里,但在这种情况下你需要给出相对目录。

1
2
--With require we use . instead of /
require("path.to.example")

现在当你试图输出test,你应该看到20。
在这种情况下,我们把test这样的变量叫做 全局变量 ,我们可以在项目的任何地方使用它们。与全局变量相反的是 局部变量 。要创建一个局部变量,我们使用关键字local。你不必刻意创建一个全局变量,在lua里,除非你使用了local,所有变量都是全局变量。

1
2
--! file: example.lua
local test = 20

这样,test就变成一个局部变量了。现在当你运行游戏,你因该又看到nil了。这是因为test作用域变了。

作用域

变量只会在它们自己的作用域里生效。加上local后,test的作用域就变成了example.lua。也就是说,test这个变量只在example.lua里有效,在其他的文件里是无效的。
如果我们在一个代码块的内部创建了一个局部变量,比如在一个函数内,一个if语句内或是一个循环内,那么这个代码块就是这个局部变量的作用域。

1
2
3
4
5
6
7
--! file: main.lua
if true then
local test = 20
end

print(test)
--Output: nil

输出test的结果是nil,因为我们试图在它的作用域之外输出他。
函数的形参就像是局部变量一样,它们只存在在函数内部。
为了真正理解作用域是怎么工作的,请看下面的代码:

1
2
3
4
5
--! file: main.lua
test = 10
require("example")
print(test)
--Output: 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
--! file: example.lua
local test = 20

function some_function(test)
if true then
local test = 40
print(test)
--Output: 40
end
print(test)
--Output: 30
end

some_function(30)

print(test)
--Output: 20

如果你运行游戏,输出应该是:40,30,20,10。下面我们来分析一下这些代码。
首先我们在main.lua里创建了test,但在输出它之前,我们require("example")
example.lua里我们创建了一个局部变量test,尽管这个testmain.lua里的test有着相同的名字,但是它们是两个不同的变量。这也意味着我们给这个局部变量test的值不会影响到那个全局变量。
我们创建了一个叫some_function(test)的函数并且随后调用了它。
函数的形参test不会影响我们之前创建的那个局部变量test
在if语句内我们创建了另一个叫test的局部变量,他不会影响到形参test
第一个被执行的print在if语句里,输出了40。显然它输出的是if语句内的局部变量test。在if语句之后我们又输出了一次test,这次是30,这是我们传递给函数的参数。形参test不会被if语句内的局部变量test影响,if语句内的局部变量test只在if语句内有效,但在if语句内,局部变量test拥有比形参test更高的优先级。(也就是说,当你在if语句内部使用test时,lua会认为你是指if语句内的局部变量test,而不是形参test或是什么别的test
然后在函数外我们又输出了一次test。这次是20。它输出的是在example.lua开头处创建的局部变量test,这个test不会被函数内的test影响。
最后我们在main.lua里又一次输出test。结果是10。全局变量test不会被example.lua里的那些局部变量test影响。
下图用颜色标注了每个test的作用域:

注意,尽管上面的示例创建了多个同名的变量,但这并不意味着我们鼓励你在实际的项目中这么做,这么做只是为了让你理解作用域。事实上,你应该尽可能避免这样做,因为这很容易引起混乱。

多说一句,当你创建一个局部变量时,你不必立刻为他赋值。

1
2
local test
test = 20

返回一个值

如果你在一个文件的顶级作用域里加入一个return语句(着意味着不是在某个函数里),那么当你用require加载这个文件时,require将返回它。
举个例子:

1
2
3
--! file: example.lua
local test = 99
return test
1
2
3
4
--! file: main.lua
local hello = require "example"
print(hello)
--Ouput: 99

什么时候需要使用局部变量?为什么要用局部变量?

你最好总是使用局部变量,这样做有几点原因。
第一点是lua访问局部变量快过全局变量。尽管差别不大,可能二者只相差了0.000001秒,但是如果你大量使用全局变量,这其中的差别会非常明显。
另一个原因是使用全局变量更容易出错。你可能在某处使用了一个变量a,然后你忘了它。于是你在接下来的某处把另一个变量也起名叫a。尽管你知道这两个a代表不同的东西,但计算机不知道。计算机会认为你首先创建了一个变量a,然后又该变了a的值。错误就这样产生了。如果一个变量只在某个地方用到,你最好使用局部变量。
在之前的章节里我们写了一个创建矩形的函数。把它找出来。在这个函数里我们就可以让rect变成局部变量,因为我们只在函数里用到它。
我们不把listOfRectangles变成局部变量,因为我们不止在love.load里用到它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function love.load()
listOfRectangles = {}
end

function createRect()
local rect = {}
rect.x = 100
rect.y = 100
rect.width = 70
rect.height = 90
rect.speed = 100

-- Put the new rectangle in the list
table.insert(listOfRectangles, rect)
end

虽然我们不能把listOfRectangles变成love.load里的局部变量,但我们可以让他变成main.lua里的局部变量。

1
2
3
4
5
6
-- By declaring it here we can access it everywhere in this file.
local listOfRectangles = {}

function love.load()
-- It's empty so we could remove this function now
end

所以有没有推荐使用全局变量的时候呢?人们有着不同的看法。有的人会说永远不要使用全局变量。但是我觉得偶尔用一下也没什么,尤其是对于一个初学者而言,当你需要在多个文件里使用某个变量时。就像love也是一个全局变量一样。但是记住,局部变量更快。
注意尽管我在教程里使用了大量全局变量,但这只是为了方便——你不要这么做。


总结

我们用require加载文件。除非你创建的是一个局部变量,当你创建了一个变量,你就可以在所有文件里使用它(当然你要require)。局部变量不会影响到它们作用域外的同名变量。尽可能使用局部变量,因为它们更快,也更不容易出错。