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
  <setup for external access>
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来打开一个已创建的弄快。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容

  • Github地址:Design_Pattern模式分类参考:设计模式及架构模式简介 设计模式可以通俗的理解为实现/...
    iOS_肖晨阅读 663评论 0 51
  • 持续学习了两个多月,印象中经历了两次比较严重的焦虑时刻。 第一次焦虑是在学习一个月后,因为有较长时间不吃...
    底层的奋进阅读 169评论 0 0
  • 致歉书! (文/赵高峰) 其实 从我遇见你的第一眼 我就知道你很美丽 这么多年 这种感觉从未有丝毫改变 隐约间 我...
    濩泽流风阅读 305评论 0 0