作用域教程

lua-users home
wiki

到目前为止,您只是为名称分配了值,并且可以在脚本中的任何位置通过使用该名称来取回该值。这对于小型示例来说是可以的,但既然您已经了解了函数,这可能会成为一个大问题:如果不同的函数使用相同的名称来存储临时值该怎么办?它们会发生冲突并相互覆盖,使您的脚本成为一个难以调试的混乱。解决方案是使用 local 关键字来控制变量的存活范围。

交互式解释器注意事项

此页面上的示例将以脚本文件的形式编写,而不是交互式解释器会话,因为在其中处理局部变量非常困难。稍后将解释原因。

创建局部变量

要创建局部变量,请在赋值前添加 local 关键字

local a = 5
print(a)

更改变量时不再需要 local 关键字

local a = 5
a = 6 -- changes the local a, doesn't create a global

局部变量仅存在于创建它们的块中。在块的外部,它们不再存在。

local a = 5
print(a) --> 5

do
  local a = 6 -- create a new local inside the do block instead of changing the existing a
  print(a) --> 6
end

print(a) --> 5

变量可见的地方称为变量的“作用域”。

现在让我们使用函数来展示这有多么有用

function bar()
  print(x) --> nil
  local x = 6
  print(x) --> 6
end

function foo()
  local x = 5
  print(x) --> 5
  bar()
  print(x) --> 5
end

foo()

如您所见,每个变量都从声明它的点到它声明的块的末尾都是可见的。尽管 bar 的 x 与 foo 的 x 同时存在,但它们不是在同一个块中编写的,因此它们是独立的。这就是所谓的词法作用域

local function 语法糖

local function f() end

-- is equivalent to

local f
f = function() end

-- not

local f = function() end

最后两个示例之间的区别很重要:局部变量在赋予它初始值的 = 的右侧仍然不存在。因此,如果函数的内容使用 f 来获取对自身的引用,它将正确地获取第一个和第二个版本中的局部变量,但第三个版本将获取全局 f(如果不是其他代码设置的完全无关的值,则为 nil)。

闭包

函数可以使用在它们外部创建的局部变量。这些被称为上值。使用上值的函数称为闭包

local x = 5

local function f() -- we use the "local function" syntax here, but that's just for good practice, the example will work without it
  print(x)
end

f() --> 5
x = 6
f() --> 6

即使变量在函数外部被更改,函数也能看到更改。这意味着函数中的变量不是副本,而是与外部作用域共享的。

此外,即使外部作用域已经结束,函数仍然会保留对该变量的引用。如果在作用域中创建了两个函数,即使外部作用域已消失,它们仍然会共享该变量。

local function f()
  local v = 0
  local function get()
    return v
  end
  local function set(new_v)
    v = new_v
  end
  return {get=get, set=set}
end

local t, u = f(), f()
print(t.get()) --> 0
print(u.get()) --> 0
t.set(5)
u.set(6)
print(t.get()) --> 5
print(u.get()) --> 6

由于对 f 的两次调用返回的两个值是独立的,我们可以看到每次调用函数时,它都会创建一个新的作用域和新的变量。

同样,循环在每次迭代时创建一个新的作用域

local t = {}

for i = 1, 10 do
  t[i] = function() print(i) end
end

t[1]() --> 1
t[8]() --> 8

为什么局部变量在交互式解释器中很困难

因为它在新的作用域中运行每一行

> local a=5; print(a)
5
> print(a) -- a is out of scope now, so global a is used
nil

您可以做的一件事是将代码包装在一个 do-end 块中,但直到您完成编写整个块之前,它都不会是交互式的

> do
>>  local a = 5
>>  print(a) -- works on a new line
>> end
5

为什么不默认使用 local?

您可能来自另一个默认情况下使变量成为局部变量的语言,并且可能在想“所有这些额外的复杂性有什么意义?为什么不默认使变量成为局部变量?”

x = 3

-- more code, you might have even forgotten about variable x by now...

function ()
  -- ...
  x = 5 -- does this create a new local x, or does it change the outer one?
  -- ...
end

-- some more code...

改变外部变量的问题在于,您可能打算创建一个新变量,但却改变了您可能甚至不知道的现有变量,从而引入了错误。

创建新变量的问题在于,如果您实际上想更改外部变量怎么办?

使用 local 关键字,一切都很明确:没有 local,您将更改现有变量;使用它,您将创建一个新变量。

有关此内容的更多讨论,请参阅 LocalByDefault

何时使用局部变量

总的规则是始终使用局部变量,除非程序的每个部分都需要访问该变量(这非常罕见)。

由于很容易忘记使用 local,而且 Lua 不会对此发出警告(而是默默地创建一个全局变量),因此它可能成为错误的根源。一个解决方案是使用像 strict.lua(如下所示)这样的脚本,它使用元表(在后续教程中介绍)来捕获全局变量的创建并引发错误。您可以将脚本放在项目中的一个文件中,然后执行 require("strict") 来使用它。

--
-- strict.lua
-- checks uses of undeclared global variables
-- All global variables must be 'declared' through a regular assignment
-- (even assigning nil will do) in a main chunk before being used
-- anywhere or assigned to inside a function.
--

local mt = getmetatable(_G)
if mt == nil then
  mt = {}
  setmetatable(_G, mt)
end

__STRICT = true
mt.__declared = {}

mt.__newindex = function (t, n, v)
  if __STRICT and not mt.__declared[n] then
    local w = debug.getinfo(2, "S").what
    if w ~= "main" and w ~= "C" then
      error("assign to undeclared variable '"..n.."'", 2)
    end
    mt.__declared[n] = true
  end
  rawset(t, n, v)
end
  
mt.__index = function (t, n)
  if not mt.__declared[n] and debug.getinfo(2, "S").what ~= "C" then
    error("variable '"..n.."' is not declared", 2)
  end
  return rawget(t, n)
end

function global(...)
   for _, v in ipairs{...} do mt.__declared[v] = true end
end

有关强制使用局部变量的更多信息,请参阅 DetectingUndefinedVariables


RecentChanges · preferences
编辑 · 历史
最后编辑于 2013 年 12 月 21 日上午 4:07 GMT (差异)