Programming in Lua(Thrid Edition)笔记

5.2k 词

14 The Environment

这章有点难理解,有些段落反复看了好多遍才感觉好像是看懂了。

  • Lua将所有全局变量存储在一个叫做global environment的table中

    1
    for n in pairs(_G) do print(n) end
  • 对于动态变量名,可动态创建chunk并编译:

    1
    2
    3
    4
    x = 1
    varname = "x"
    value = loadstring("return " .. varname)
    print(value())

这样的消耗较多,可用global environment:

1
2
val = _G[varname]
print(val)

  • _G["a"] = _G["var1"]写法繁琐,应简写为a = var1

  • _G["io.read"]无法获得iotable的read域,可用以下getfield()实现:

    1
    2
    3
    4
    5
    6
    7
    8
    function (f)
    local v = _G -- start with the table of globals
    for w in string.gmatch(f, "[%w_]+") do
    v = v[w]
    end
    return v
    end
    getfield("io.write")("1n")
  • 通过_G设置多级全局变量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function setfield(f, v)
    local t = _G -- start with the table of globals
    for w, d in string.gmatch(f, "([%w_]+)(%.?)") do
    if d == "." then -- not last name?
    t[w] = t[w] or {} -- create table if absent
    t = t[w] -- get the table
    else -- last name
    t[w] = v -- do the assignment
    end
    end
    end
    setfield("t.x.y", 1)
    print(t.x.y)
    print(getfield("t.x.y"))
  • 全局变量声明,因为全局变量存储在table_G中,所以可以通过metatable处理未定义元素的操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    setmetatable(_G, {
    __newindex = function (t, n, v)
    error("attempt to write to undeclared variable " .. n, 2)
    end,
    __index = function (_, n)
    error("attemp to read undeclared variable " .. n, 2)
    end,
    })
    print(a) --> stdin:1: attempt to read undeclared variable a

rawset()声明新变量,initval or false是为了使新的全局变量总会得到一个不同与nil的值:

1
2
3
function declare(name, initval)
rawset(_G, name, initval or false)
end

用debug库限制新的全局变量的声明只能在main chunk中,debug.getinfo(2, "S")返回一个table,其what域说明metamethd调用是在main chunk、普通Lua函数和C函数其中之一

1
2
3
4
5
6
7
8
9
10
11
12
13
__newindex = function (t, n, v)
local w = debug.getinfo(2, "S").what
if w ~= "main" and w ~= "C" then
error("attemp to write to undeclared variable " .. n, 2)
end
rawset(t, n, v)
end
a = 1
print(a)
function b()
c = 2
end
b() --> stdin:1: attemp to write to undeclared variable c

判断一个变量是否存在,不能通过与nil比较,因为__index会报错,取而代之,我们可以用rawget()

1
2
3
4
if rawget(_G, var) == nil then
-- 'var' is undeclared
...
end

为了使声明为nil的变量不被Lua认为是未声明的,可以用一个table保存声明的变量的名字,每次调用metamethod时都检查一下变量是否在该table中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local declaredNames = {}
setmetatable(_G, {
__newindex = function (t, n, v)
if not declaredNames[n] then
local w = debug.getinfo(2, "S").what
if w ~= "main" and w ~= "C" then
error("attempt to write to undeclared variable " .. n, 2)
end
declaredNames[n] = true
end
rawset(t, n, v) -- do the actual set
end,
__index = function (_, n)
if not declaredNames[n] then
error("attempt to read undeclared variable " .. n, 2)
else
return nil
end
end,
})
a = nil
print(a) --> nil
print(b) --> stdin:1 attempt to read undeclared variable b

  • 非全局环境,所谓非全局环境,就是指之前所说的全局环境并非真正的全局环境,是Lua实现的伪全局环境,是为了操作全局变量更方便。

  • free name是并非绑定在一个特定声明的名字,即并非出现在有这些名字的局部变量所在的域(可以简单理解为全局变量的名字),例如var1 = var2 + 3这个chunk里的var1var2就是free name,Lua会将该chunk编译为:

    1
    2
    3
    4
    local _ENV = <some code>
    return funtion (...)
    _ENV.var1 = _ENV.var2 + 3
    end

也就是说Lua会把chunk编译在一个预定义的upvalue_ENV的域中。load()则会把_ENV初始化global environment

1
2
3
4
local _ENV = <the global environment>
return function (...)
_ENV.var1 = _ENV.var2 + 3
end

  • 总结一下Lua处理全局变量的方式:

    • 将任何chunk编译在一个upvalue_ENV的域中
    • 将任何free namevar变为_ENV.var(简单来说,就是_ENV只会记录全局变量,而不记录局部变量)
    • load()loadfile()将一个chunk的第一个upvalue初始化为global environment
  • 通过_ENV = nil阻止在之后的chunk中获取全局变量,可以用来控制代码可以使用什么变量

    1
    2
    3
    4
    5
    local print, sin = print, math.sin
    _ENV = nil
    print(13) --> 13
    print(sin(13)) --> 0.42016703682664
    print(math.cos(13)) -- eror!
  • _ENV绕过局部变量

    1
    2
    3
    4
    a = 13 -- global
    local a = 12
    print(a) --> 12 (local)
    print(_ENV.a) --> 13 (global)
  • _ENV改变环境

    1
    2
    3
    4
    -- change current environment to a new empty table
    _ENV = {}
    a = 1 -- create a field in _ENV
    print(a) --> stdin:4: attempt to call global 'print' (a nil value)

用旧环境填充新环境:

1
2
3
4
5
a = 15 -- create a global variable
_ENV = {g = _G} -- change current environment
a = 1 -- create a field in _ENV
g.print(a)
g.print(g.a) --> 15

_G代替g

1
2
3
4
5
a = 15 -- create a global variable
_ENV = {_G = _G} -- change current environment
a = 1 -- create a field in _ENV
_G.print(a)
_G.print(_G.a) --> 15

用继承来填充新环境:

1
2
3
4
5
a = 1
local newgt = {} -- create new environment
setmetatable(newgt, {__index = _G})
_ENV = newgt -- set it
print(a)

这里只设置了__index,所以任何赋值操作都发生在新环境中,所以错误地改变了全局环境的一个变量没有危险,仍可以通过_G来活取原值

1
2
3
4
5
6
-- continuing previous code
a = 10
print(a) --> 10
print(_G.a)
_G.a = 20
print(_G.a) --> 20

  • _ENV符合通常的域规则,一个chunk中的函数可以像获取其他外部变量那样活取_ENV

    1
    2
    3
    4
    5
    6
    7
    8
    _ENV = {_G = _G}
    local function foo()
    _G.print(a) -- compiled as '_ENV._G.print(_ENV.a)'
    end
    a = 10 -- _ENV.a
    foo() --> 10
    _ENV = {_G = _G, a = 20}
    foo() --> 20
  • 用局部的_ENV生成一个局部环境:

    1
    2
    3
    4
    5
    6
    a = 2
    do
    local _ENV = {print = print, a = 14}
    print(a) --> 14
    end
    print(a) --> 2 (back to the original _ENV)
  • 定义一个有私有环境的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function factory(_ENV)
    return function ()
    return a -- "global" a
    end
    end
    f1 = factory{a = 6}
    f2 = factory{a = 7}
    print(f1()) --> 6
    print(f2()) --> 7

这里factory()返回一个闭包,其upvalue为_ENV。用这种方法可以让一些函数共享一个公共的环境,或者让一个函数专门用于改变这些函数共享的这个环境

  • load()的可选的第四个参数用来设定_ENV的值,loadfile()则为第三个参数

  • 使用配置文件:

    1
    2
    3
    4
    -- file 'config.lua'
    width = 200
    height = 300
    ...

可用一下代码加载:

1
2
3
env = {}
f = loadfile("config.lua", "t", env)
f() -- only after executing will env be populated

配置文件中的代码会运行在一个空环境中,并且所有配置都会存储在该环境中,对其他代码无影响,有一定安全性

  • load()loadfile()返回的函数可多次调用,为了使每次调用时的环境不同,可用以下两种方法:

  • 法一:debug.setupvalue()可以改变一个函数的任何upvalue

    1
    2
    3
    4
    f = loadfile(filename)
    ...
    env = {}
    debug.setupvalue(f, 1, env)

debug.setupvalue()的第一个参数是一个函数,第二个参数是upvalue索引,第三个参数是新的upvalue,此处第二个参数总为1,因为Lua保证load()loadfile()返回的函数只有一个upvalue,即_ENV。此法破坏了Lua的可见性规则:一个局部变量不能被外部获取

  • 法二:用可变参数,用Exercise 8.1loadwithprefix()在加载的chunk之前加上local _ENV = ...;
    1
    2
    3
    4
    f = loadwithprefix("local _ENV = ...;", io.lines(filename, "*L"))
    ...
    env = {}
    f(env)