lua 环境中变量的作用范围剖析

lua 能够执行不同文件中的 lua 代码,这些分散在不同 .lua 文件中的 lua 代码所创建和使用的 lua 变量,它们有着不同的作用范围,本文将结合 lua 编译器行为进行详细分析。

  • chunkblock

    • chunk

      chunk 指一段 lua 能够执行的代码,这段代码可能有一行,也可能有多行。
      lua 解释器的交互环境中,lua 视每一行用户输入为一个 chunk*。
      当 *lua
      执行 .lua 文件时,lua 视整个文件为一个 chunk*。
      *lua
      chunk 为单位执行 lua 代码。
    • block

      luablock 来区分变量的作用范围。
      block 分三类:
      body of a control structure (控制结构体),包括 if then elsewhilerepeatfordo end等。
      body of a function(函数体),函数定义与函数结束之间。
      *chunk
      。根据 *chunk
      定义,chunk 内能够包含任何合法的 lua 代码,chunk 内部能够包含多个控制结构体和函数体,因此,定义在 chunk 内的变量能够被内部其他 block 使用。
  • 局部变量、自由名称和上游值

    lua 变量不需要 declaration 声明,赋值即创建,不限使用(尽力取值),如:
    b = 10 -- 创建变量 b
    print(a) -- 变量 a 不存在,返回 nil
    lua 变量的作用范围是 block,在变量作用范围的说明中,涉及局部变量、自由名称和上游值的概念,现一一说明。
    • 局部变量

      局部变量 local variable 的创建形式为 local varname = ?。局部变量的作用范围是创建时,该变量所在的 block
    • 自由名称

      自由名称 free name 的创建形式为 freename = ?。需要注意的是,该变量在之前并没有被创建,如:
      local a = 5
      a = 4 -- a 在之前已创建,a 是局部变量
      b = "hello" -- b 是自由名称
      自由名称由 lua compiler 编译器通过内部机制转换成类局部变量,对整个 chunk 起作用。
    • 上游值

      上游值 upvalue*,由 *lua compiler 管理和设置。
      lua 执行 chunk 时,lua compiler 会为该 chunk 创建一个局部变量 _ENV,并赋值 _ENV = upvaluechunk 内的全部自由名称的访问将被转换为对局部变量 _ENV 成员的访问,例如:
      a = 5
      print(b)
      -- 将被转换为
      _ENV.a = 5
      print(_ENV.b)
  • 变量 _ENV

    • 环境变量 _ENV

      类似于操作系统的环境变量,在 lua 运行环境中,_ENV 的值确定了当前 chunk 的运行环境。
      a = 5
      print(a)
      -- 真实的变量访问
      _ENV.a = 5
      _ENV.print(_ENV.a)
      可通过如下代码查看 lua 默认 upvalue
      for k, v in pairs(_ENV) do
      print(k, v)
      end
      执行上述代码后,可看到熟悉的名称,如:package、math、string、print、pairs 等。可以看出,有了 _ENV 的支持,才能简洁无障碍的使用这些系统 modulefunction
    • 可控环境 _ENV

      chunk 运行依赖 _ENV,通过 _ENV 设置可实现 lua 运行环境的隔离和修改。
      改变默认 _ENV 成员,裁减、增加或修改所需的 modulefunction*,改变 *chunk 级别的运行环境,如:
      -- 删除 dofile 函数
      dofile = nil
      _ENV.dofile = nil
      -- 增加自定义模块
      mymodule = require("modulename")
      _ENV.mymodule = _ENV.require("modulename")
      -- 修改 print 函数
      print = function() --[[your code]] end
      _ENV.print = function() --[[your code]] end

      -- 也可进行整体替换
      _ENV = {print = print} -- 该环境只支持 print 函数
      创建局部变量 _ENV,改变 block 级别的运行环境,如:
      do
      -- _ENV 仅在此 block 有效
      local _ENV = {...}
      ...
      end

      function f(para, _ENV)
      -- 以函数参数形式使用 _ENV
      end

      function f(para)
      -- 以局部变量的形式使用 _ENV
      local _ENV = {...}
      end
    • free name 传导和 _ENV._G 成员

      lua 运行时,默认 upvalue 会因 free name 的创建而发生改变,而这种改变将传导至下一次的 upvalue 使用,从而出现全局变量的效果。如:
      -- chunk1 c1.lua
      a = 5

      -- chunk2 c2.lua
      print(a)

      -- chunk3 c3.lua
      dofile("c1.lua") -- 执行 chunk1,遇到 free name:a,创建 upvalue.a,upvalue.a = 5
      print(a) -- upvalue 已被 chunk1 改变,upvalue.a 值为 5
      dofile("c2.lua") -- upvalue 传导至 chunk2,获取 upvalue.a
      从执行效果看,以 upvalue 为桥梁,free name 的使用会出现全局变量的效果。
      lua 默认 _ENV 包含 _ENV._G 成员,而 _ENV._G 的值为默认 upvalue*,即 _ENV == _ENV._G,因此,可以通过 _G.x 来引用“全局变量” x
      需要注意的是,无节制的使用 *free name
      将导致 upvalue 的成员过多,影响 upvalue 传导时的效率。实际编程过程中,可使用 _G 来明确全局变量的使用,本地使用的变量一律用 local 来修饰,如:
      local a = 5 -- 局部变量 a
      local b = 6 -- 局部变量 b
      _G.c = 20 -- 显式使用全局变量 c
      此外,如果 _ENV 的值发生改变,_ENV._G 可能不存在,因此,在改变 _ENV 之前,可事先保存 _ENV 或者 _ENV._G,如:
      local g = _G -- _ENV 未改变,保存 _G 至局部变量 g
      _ENV = {print = print, a = "hello"} -- 新 _ENV 只包含两个成员:_ENV.print 和 _ENV.a
  • 结束语

    lua 局部变量的作用范围是 block*。
    变量 _ENV 由 *lua compiler
    在执行 chunk 前创建,可理解为该 chunk 的局部变量,chunk 内对 free name 的访问被转换为对 _ENV 对应成员的访问。ENV 的默认值为 upvalue*。
    *lua
    没有全局变量,lua 通过 upvalue_ENV 实现全局变量的效果。