1环境
我们在lua中使用的所有全局变量,其实都保存在一个常规的table
中,这个table
被称为环境(environment)。由于他是一个table
,所以我们可以像操作其他table
一样操作他。为了变故实施这种操作,lua
将环境table
自身保存在一个全局变量_G
中。,比如我们可以:
for n in pairs(_G) do
ptinr(n)
end
-
1. 具有动态名字的全局变量
有时候当我们需要访问或者设置全局变量的时候,也会用到一种元编程的形式。例如下面的操作
value=loadstring("return "..varname)() --varname:变量名称
但其实我们可以用以下方式直接访问
_G[varname]=value
-
2. 全局变量声明
由于全局变量只是存放在一个普通的table
中,那意味着我们可以通过元表来修改其一些访问的行为。
例如说
setmetatable(_G,{
__newindex=function(_,n)
error("attempt to write to undeclared value "..n,2)
end,
__index=function(_,n)
error("attempt to read a undeclared value "..n,2)
end,
})
这段代码让我们对所有全局table
中不存在的key
访问都会引发一个错误
但是这样的话其实我们也无法去声明一个新变量。所以声明的过程就需要绕过元表,例如之前的rawset
和rawget
-
3. 非全局的环境
之前的环境又一大问题在于他是全局的。局部修改之后全局都会生效,例如我们让全局元表设置为上面的控制全局变量声明访问,则一旦程序中有地方不遵守这个规范,程序就发生错误了。在Lua5
中对这个问题做了改进,他允许每个函数都拥有一个自己的环境来查找全局的变量。这项机制之后会具体讲,他们的好处在于能使全局变量访问任何地方。
我们可以通过setfenv
来改变一个函数的环境。第一个参数除了可以指定为函数本身,也已用数字代替。比如1表示当前函数,2表示调用此函数的函数,以此类推。 刚开始尝试可能会发生一下的错误
a=1 --全局变量
setfenv(1,{}) --将当前的全局环境变为一个空的table
print(a) --报错 attempt to call global 'print'(a nil value)print在新的table中不存在
一旦改变了环境,所有的全局访问就会使用新的table。如果新的table是空的,那么就会丢失所有的全局变量,包括_G
例如之前的我们可以修改为
a=1
setfenv(g=_G)
g.print(a) --nil
g.print(g.a) --输出1
也可以使用_G
来代替g
,如:
a=1
setfenv({1,{_G=_G})
_G.print(a) --nil
_G.print(g.a) --输出1
对于Lua
来说,_G
只是一个普通的名字。当lua
创建最初的全局table
时,只是将这个table
赋予给了全集变量_G
,lua
不会在意这个全局变量_G
的当前值。setfenv
不会在新环境中设置这个变量。但如果希望这个新环境引用最初的全局table
,一般使用_G
这个名称即可。
另外一种组装新环境的方式是继承
a=1
local newgt={} --新环境
setmetatable(newgt,{__index=_G})
setfenv(1,newgt) --设置环境
print(a)
这段代码中,新环境从原来的环境中继承了print
和a
然而任何赋值都发生在新的table
中
下面我们讲一个设计closure的例子
function factory()
return function()
return a --返回全局a的值
end
end
a=3
f1=factory()
f2=factory()
print(f1()) -->3
print(f2()) -->3
setfenv(f1,{a=10})
print(f1()) -->10
print(f2()) -->3
这里的factory创建了一个简单地closure
,这个closure
用于返回他的全局a的值,每次调用factory都会创减一个新的闭包和属于该闭包的环境。每个新创建的闭包都继承了创建它的闭包的函数环境。
由于函数继承了创建其函数的环境。所以一个程序块人如果改变了它自己的环境,那么后续由他创建的函数也都共享这个新环境,这项机制对创建命名空间是很有用的。
2.模块与包
模块系统的一个主要目的是允许以不同的形式来共享代码。但若没有一项公共的规则就无法实现这样的共享。lua5.1
开始,为模块与包定义了一系列的规则。这些规则不需要语言引入额外的技能,通过table
,函数,元表来实现这些规则。这其中有两个重要函数可以容易的通过这些规则分别是require(用于使用模块)
和module(用于创建模块)
。
从用户的观点来看,一个模块就是一个程序库,可以通过require
来加载。然后便得到了一个全局变量,表示一个table
。这个table就是一个命名空间,其内容就是模块中导出的所有东西(函数、常量)。一个规范的模块还应使用require
来返回这个table
例如我们要调用一个模块中的函数
require "mod"
mod.foo()
或者
local m=require "mod"
m.foo()
还可以为一些函数提供别名
require mod
local f=mod.foo()
f()
1.require函数
对于require
而,一个模块就是一段定义了一些值的代码。
当需要加载一个模块时,只需要简单地调用require "<模板名>"
该调用会返回一个由模块函数组成的table
,并且还会定义一个包含该table
的全局变量。
即使知道某些用到的木块可能已经加载了,但只要用到require
就是一个良好的编程习惯。不过可以将标准库排除在之外,因为lua
会预先加载他们。不过你也可以为标准库的模块使用显示的require
local m=require "io"
m.write("hello world\n")
下面详细说明了require
的行为
function require(name)
if not package.loaded[name] then --检查是否已经加载该模块
local loader =findloader(name)
if loader==nil then
error("unable to load module "..name)
end
package.loaded[name]=true
local res=loader(name)
if res ~= nil then
package.loaded[name] = res
end
end
return package.loaded[name]
end
首先检查是否加载过,如果加载过直接返回已经加载成功的。若没有查找是否存在,然后返回。require
不会去进行重复加载。只要一个模块已经加载过,后续的require
调用豆浆返回同一个值。
如果模块尚未加载,require
就是这为该模块找到一个加载器(loader),会现在table package.prelado
中查询传入的模块名。如果在其中找到了一个函数,就会以该函数作为模块的加载器。通过preload table
,就有了一种通用的方法来处理各种不同的情况。
如果require
找到了一个lua
文件,他就通过 loadfile
来加载文件。而如果找到的是一个C程序库,就通过loadlib
来加载。这两个函数都只是加载了代码,并没有运行。为了运行代码,require
会以模块名作为参数来调用这些代码。如果加载器有返回值,require
九江这个返回值存储到 table package.loaded
中,如果没有返回值,require
就会回table package.loaded
中的值。
这里有一个细节是调用加载器之前,先将package.loaded[name]=true
,这样避免了A加载B,B又要加载A这种情况出现的无限循环。
如果想要强制使用require
对一个库加载两次的话,可以简单地删除package.loaded
中的模块条目。例如,在成功地require "foo"
之后,package["foo"]
就不问nil了。下面代码可以再次加载该模块
package.loaded["foo"]=nil
require "foo"
require
用于搜索lua
文件的路径存放在变量package.path
中。当lua
启动后,便以环境变量LUA_PATH
的值来初始化这个变量.
2.编写模块的基本方法
在lua
中创建一个模块最简单的方法是:创建一个table
,并将所有需要导出的函数放入其中,最后返回这个table
。
complex={}
function complex.new(r,i) return {r=r,i=i} end
complex.i=complex.new(0,1)
function complex.add(c1,c2)
return complex.new(c1.r+c2.r,c1.i+c2.i)
end
function complex.sub(c1,c2)
return complex.new(c1.r-c2.r,c1.i-c2.i)
end
function complex.mul(c1,c2)
return complex.new(c1.r*c2.r-c1.i*c2.i,c1.r*c2.i+c1.i*c2.r)
end
local function inv(c) --声明称局部变量,就是定义成一个私有名称
local n=c.r^2+c.i^2
return complex.new(c.r/n,-c.i/n)
end
function complex.div(c1,c2)
return complex.mul(c1,inv(c2))
end
return complex
上面的例子在使用table
编写模块时,没有提供与真正模块完全一致的功能性,首先,必须现实地将模块名放到每个函数定义中。其次,一个函数在调用同一模块中的另一个函数是,必须限定被调用的函数的名称。可以使用一个固定的局部名称(例如M
)来定义和调用模块类的函数,然后将这个局部名称赋予模块的最终名称。通过这种方式,可以将上例改写为
local M={}
complex=M
M.i={r=0,i=1}
function M.new(r,i) return {r=r,i=i} end
complex.i=complex.new(0,1)
function M.add(c1,c2)
return M.new(c1.r+c2.r,c1.i+c2.i)
end
3.使用环境
函数环境是一种有趣的技术,基本想法就是让模块的主程序块有一个独占的环境。这样不仅它的所有函数都可以共享这个table
,而且它的所有全局变量也都记录在这个table
中。还可以将所有公有函数生命为全局变量,这样它们就都自动地记录在一个独立的table
中了。模块所要做的就是这个table
赋予模块名和package.loaded
下面上一个例子
local modname=...
local M={}
_G[modname]=M
package.loaded[modename]=M
setfenv(1,M)
此时我们再声明函数add
的时候,他就成了之前的形式。应为此环境已经改变。
用此种方法之后,即使忘记填写local属性,那么也不会污染全局命名空间。只会把一个函数由私有变成共有。但是现在还有一个问题,就是访问其他的模块。当创建了一个空的table M
作为环境之后,就无法访问前一个环境中的全局变量了。下面给出几种方法,各有优缺点。
local modname=...
local M={}
_G[modname]=M
package.loaded[modname]=M
setmetatable(M,{__index=_G})
setfenv(1,M)
必须先调用setmetatable
再调用setfenv
,因为通过这种方法,模块就能直接访问任何全局标识了,每次访问只需付出很小的开销。这种导致的结果就是 从概念上来说,此时的模块中包含了所有的全局变量。
local modname=...
local M={}
_G[modname]=M
package.loaded[modname]=M
local _G=_G
setfenv(1,M)
刺种方法相当于在模块内,用局部变量保存了一份对全局的引用,使用的时候只要通过_G.fxx()
即可。这种方法比上面的那种更快,因为无需访问元方法
local modname=...
local M={}
_G[modname]=M
package.loaded[modname]=M
local sqrt=math.sqrt
local io=io
setfenv(1,M)
这种方法是在改变环境之前,预先将所有内部需要用到的外部变量事先加以引用,比起之前的两种更加正规。
4.module函数
我们通过之前的例子可以发现,开头的代码比较相似
local modname=...
local M={}
_G[modname]=M
package.loaded[modname]=M
setfenv(1,M)
在lua5.1
之后,提供了一个新函数module,它囊括了以上的功能。再开始编写一个模块时,可以通过module(...)
直接完成
默认的情况下,module
不提供外部的访问,必须在调用它之前,为需要访问的外部函数或模块声明适当的局部变量。也可以通过继承来实现外部访问。只需在调用module
时加一个选项package.seeall
相当于
module(...,{__index=_G})
module(...,package.seeall)
module
在创建模块table
之前,会先检查package.laoded
是否已经包含了这个模块,或者是否已经存在于模块同名的变量。如果module
由此找到了这个table
,它就会复用该table
作为模块。yejiushishuo9,可以用module
来打开一个已创建的弄快。