lua入门笔记4 环境 模块与包

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访问都会引发一个错误
但是这样的话其实我们也无法去声明一个新变量。所以声明的过程就需要绕过元表,例如之前的rawsetrawget

  • 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)

这段代码中,新环境从原来的环境中继承了printa然而任何赋值都发生在新的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来打开一个已创建的弄快。

你可能感兴趣的:(lua入门笔记4 环境 模块与包)