Torch7 系列教程之Lua语言学习教程(三)

3.Lua进阶(二)

这一章主要学习Lua中数据结构、Metatables、数据文件与持久化、Lua环境内容。

3.1 数据结构

Lua语言中唯一的数据结构是table,例如其他语言中arrays、records、lists、queues、sets等等,都是由Lua中的table来实现的。C语言和类似C语言(例如Pascal语言)经常使用arrays 和指针来实现多种数据结构,Lua语言中table实现这些数据结构的功能更加强大。接下来我们将学习一些高级的table数据结构的知识。
数组
数组在Lua语言中通过访问表中的元素即可以实现数组的依次访问,table的大小可以根据需要动态增长。例如初始化一个一维数组:

a = {}
for i =1,1000 do
	a[i] = i
end
-- 创建好了一个一维数组a

另外,数组的下标可以不是正整数,也可以从负数开始。然而在Lua中习惯上数组的下标从1开始,Lua的标准库与此习惯一致,因此从1开始可以使用与标准库函数一致的数组,否则就无法使用。在构造器中初始化数组,例如:

suqares = {1,4,9,16,25,36,49,64,81}

阵和多维数组
下面是一个创建多维数组的一个例子:

mt = {}
for k = 1,N do
	mt[k] = {}
	for j=1,M do
		mt[i][j] = 0
	end
end

其中多维数组的创建是通过每一行创建一个table来实现多维数组的创建,另外一种方法是将行和列组合起来实现一个多维矩阵:

mt = {}
for k = 1,N do
	for j = 1,M do
		mt[i*M + j] = 0
	end
end

table可以很好地存储稀疏矩阵的数据。
链表
Lua中实现的链表方式是通过table来实现链表,即每一个节点是一个table,指针是这个表中的一个域,并且指向另一个节点(table)。举个例子:

-- 根节点
list = nil
-- 在链表开头插入一个值为v的节点
list = {next = list , value = v}
-- 遍历整个列表的操作
local  l = list
while l do
	print(l.value)
	l = l.next
end

另外双向链表和循环列表也是很容易实现的,一般情况下,可以使用一个非常大的数组lai8表示栈,其中一个域n指向栈顶。
队列和双端队列
虽然可以使用Lua中的table库提供的insert和remove操作来实现队列,但是这种方式实现队列针对大量的数据时候效率太低,有效的方式是使用两个索引下标,一个表示第一个元素,另一个表示最后一个元素。举个例子,为防止污染全局变量,我们可以使用这样的方法来实现一个队列的操作:

-- 创建一个空队列
List = {}
function List.new()
	return {first = 0,last = -1}
end
-- 在队列的两端进行插入和删除操作
function List.pushleft(list,value)
	local first = list.first - 1
	list.first = first
	list[first] = value
end
function List.pushright(list,value)
	local last = list.last + 1
	list.last = last
	list[last] = value
end
function List.popleft(list)
	local first = list.first
	if first > list.last then
		error("list is empty")
	end
	local value = list[first]
	list[first] = nil 		--注意这里是将这个变量被垃圾回收
	list.first = first + 1
	return value
end
function List.popright(list)
	local last = list.last
	if list.first > last then
		error("list is empty")
	end
	local value = list[last]
	list[last] = nil
	list.last = last -1
	return value
end

集合和包
下面是通过构造一个函数额方式来表达一个集合的效果:

function Set(list)
	local set = {}
	for _,l in ipairs(list) do
		set[l] = true
	end
end
reserved = Set{"red","yellow","green","white"}
-- 打印集合中的这些元素信息
for v in pairs(reserved) do
	print(v)
end

字符串缓冲
例如以下的字符串逐行读取代码:

local buff = ""
for line in io.lines() do
	buff = buff .. line .. "\n"
end

看上去这几行代码并没有什么问题,但是Lua中处理效率十分低下,处理大文件的时候,Lua垃圾回收算法会遍历所有的数据结构去释放垃圾数据,这样Lua垃圾回收机制会遍历冗余的数据,使得算法效率低下。其中Java中虚拟机中也会有这样的问题,但是专门提供的StringBuffer可以很好地改善这种情况。
我们可以采用这样的一个想法来改善这个问题:用一个栈,在栈的底部用来保存已经生成的大的字符串,而小的串从栈顶入栈。

function newStack()
	return {""}
end
function addString(stack,s)
	table.insert(stack,s)
	for i = table.getn(stack) - 1, 1,-1 do
		if string.len(stack[i]) > string.len(stack[i+1]) then
			break
		end
		stack[i] = stack[i] .. table.remove(stack)
	end
end
-- 最后合并所有的字符串即可
local s = newStack()
for line in io.lines() do
	addString(s,line .. "\n")
end
s = tostring(s)

3.2 Metatables 和Metamethods

Lua中的table由于定义的行为,可以对key-value执行加操作,访问key对应的value,遍历所有的key-value。但是并不可以对两个table执行加的操作,也不能比较大小。所以,Metatables的出现允许改变table的行为,例如可以定义Lua如何计算两个table的相加操作a+b。当Lua试图对两个表进行相加时候,它会检查两个表中是否有一个表是Metatable,并且检查 Metatable是否含有__add域。如果找到则会调用__add函数(即所谓的Metamethod)计算结果。
Lua中的每一个表都有其Metatable。(之后的userdata将会看到也有Metatable),Lua默认创建一个不带metatable的新表。
有两个很重要的函数来处理元表:

  • setmetatable(table,metatable): 对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
  • getmetatable(table): 返回对象的元表(metatable)。
t = {}
print(getmetatable(t))		--返回nil值(空)
t1 = {}
setmetatable(t,t1)
assert(getmetatable(t) == t1)		-- 使用setmetatable函数设置或者改变一个表的metatable

任何一个表都可以是其他表的metatable,一组相关的表可以共享一个metatable(描述它们的共同行为)。一个表也可以是自身的metatable(描述其私有行为)。
算术运算的Metamethods
我们使用table来描述结合,使用函数来描述集合的并操作,交操作,like操作。我们在一个表内定义这些函数,然后使用构造函数创建一个集合:

Set = {}
function Set.new(t)
	local set = {}
	for _,l in ipairs(t) do
		set[l] = true
	end
	return set
end
function Set.union(a,b)
	--这里增加一个小的技巧,如果出现两个元素a和b之间的类型不同的话,必须要有判定条件
	if getmetatable(a) ~= Set.mt or getmetatable(b) ~=Set.mt then
		error("attempt to `add' a set with a non-set value",2)
	end
	local res = Set.new{}
	for k in pairs(a) do res[k] = true end
	for k in pairs(b) do res[k] = true end
	return res
end
function Set.intersection(a,b)
	local res = Set.new{}
	for k in pairs(a) do
		res[k] = b[k]		--注意这里集合的交操作,不存在的变量会赋值为nil,集合中即为无此元素
	end
	return res
end
function Set.tostring(set)
	local s = " { "
	local sep = ""
	for e in pairs(set) do
		s = s .. sep .. e
		sep = " , "
	end
	return s .. " } "
end
function Set.print(s)
	print(Set.tostring(s))
end

如此我们想增加运算符(+)作为执行两个集合的并操作,我们将所有集合共享一个metatable,并且为这个metatable添加如何处理相加的操作。
注意Entry{…}与 Entry({…})等价,他是一个以表作为唯一参数的函数调用。
第一步,我们定义一个普通的表,用来作为metatable。为了避免污染命名空间,我们将其放在set内部。
第二步,修改set.new 函数,增加一行,创建表的时候同时指定对应的metatable。

Set.mt = {}		--sets集合的metatable
function Set.new(t)
	local set = {}
	setmetatable(set,Set.mt)
	for _,l in ipairs(t) do set[l] = true end
	return set
end

所以这样,set.new创建所有的集合都有相同的metatable上了:

s1 = Set.new{10,20,30,40,50}
s2 = Set.new{60,90,80}
print(getmetatable(s1))
print(getmetatable(s2))

实际上,这样创建的集合都是对Set.mt的弱引用。
第三步,给metatable增加__add函数

Set.mt.__add = Set.union

所以说Lua试图对两个集合相加时候,将调用这个函数,以两个相加的表作为参数。
通过metamethod,我们可以对两个集合进行相加的操作,如下面的实例:

s3 = s1 + s2
Set.print(s3)

同样也可以作用于交

Set.mt.__mul = Set.intersection
Set.print((s1+s2)*s1)

注意,如果出现类型不匹配的话,例如s = s + 8,程序中会抛出异常错误
关系运算中的Metamethods
Metatables也允许我们使用metamethods:__eq(等于),__It(小于),__le(小于)给关系运算符赋予了特殊的含义。Lua中对于剩下的三个关系运算符并没有专门的metamethod,Lua将a~=b转化为not(a==b);a>b转换为b<a;a>=b转换为b<=a。实例如下描述:

Set.mt.__le = function (a,b)
	for k pairs(a) do
		if not b[k] then
			return false
		end
	end
	return true
end
Set.mt.__lt = function (a,b)
	return a <= b and not (b<=a)
end
Set.mt.__eq = function (a,b)
	return a<=b and b<=a
end
s1 = Set.new{2,4}
s2 = Set.new{4,10,2}
print(s1 <= s2)		-- true
print(s1 < s2)		-- true
print(s1 >= s1)		-- true
print(s1 > s1)		-- false
print(s1 == s2*s1)	-- true

注意:与算术运算的 metamethods 不同,关系元算的 metamethods 不支持混合类型运算。
lua中操作函数列表如下描述

模式描述
__add对应于运算符“+”
__sub对应于运算符“-”
__mul对应于运算符“*”
__div对应于运算符“/”
__mod对应于运算符“%”
__unm对应于运算符“-”
__concat对应于运算符“…”
__eq对应于运算符“=”
__lt对应于运算符“<”
__le对应于运算符“<=”

库定义的Metamethods
在一些库中,在自己的 metatables 中定义自己的域是很普遍的情况。到目前为止,我们看到的所有 metamethods 都是 Lua 核心部分的。有虚拟机负责处理运算符涉及到的metatables 和为运算符定义操作的 metamethods。但是,metatable 是一个普通的表,任何人都可以使用。
典型例子:tostring函数。
注意:print 函数总是调用 tostring 来格式化它的输出。然而当格式化一个对象的时候,tostring 会首先检查对象是否存在一个带有__tostring 域的 metatable。如果存在则以对象作为参数调用对应的函数来完成格式化,返回的结果即为tostring 的结果。如以下的例子:

Set.mt.__tostring = Set.tostring
s1 = Set.new{10,4,5}
print(s1)

setmetatable/getmetatable 函数也会使用metafield,在这种情况下,可以保护metatables。假定你想保护你的集合使其使用者既看不到也不能修改 metatables。如果你对metatable设置了__metatable 的值,getmetatable 将返回这个域的值,而调用 setmetatable将会出错:

Set.mt.__metatable = "not your business"
s1 = Set.new{}
print(getmetatable(s1))		--则打印not your bussiness
setmetatble(s1,{})
-- 会出现以下的错误
-- stdin:1: cannot change protected metatable

表相关系的metamethods
针对在两种正常状态:表的不存在的域的查询和修改,Lua 也提供了改变 tables 的行为的方法。

  1. __index metamethod
    例如下面的实例
Window = {}
Window.prototype = {x=0,y=0,width=100,height=10,}
Window.mt = {}
function Window.new(o)
	setmetatable(o,Window.mt)
	return o
end
-- 定义__index method:
Window.mt.__index = function (table,key)
	return Window.prototype[key]
end
-- 这样可以使用
w = Window.new{x=20,y=20}
print(w.width)		--打印100

当 Lua 发现 w 不存在域 width 时,但是有一个 metatable 带有__index 域,Lua 使用w(the table)和 width(缺少的值)来调用__index metamethod,metamethod 则通过访问原型表(prototype)获取缺少的域的结果。
__index metamethod 在继承中的使用非常常见,所以 Lua 提供了一个更简洁的使用方式。__index metamethod 不需要非是一个函数,它也可以是一个表。但它是一个函数的时候,Lua 将 table 和缺少的域作为参数调用这个函数;当它是一个表的时候,Lua 将在这个表中看是否有缺少的域。

  1. __newindex method
    __newindex metamethod 用来对表更新,__index 则用来对表访问。当你给表的一个缺少的域赋值,解释器就会查找__newindex metamethod:如果存在则调用这个函数而不进行赋值操作。像__index 一样,如果 metamethod 是一个表,解释器对指定的那个表,而不是原始的表进行赋值操作。另外,有一个 raw 函数可以绕过metamethod:调用rawset(t,k,v)不掉用任何 metamethod 对表 t 的 k 域赋值为 v。__index 和__newindex metamethods 的混合使用提供了强大的结构:从只读表到面向对象编程的带有继承默认值的表。
  2. 具有默认值的表
    在一个普通的表中任何域的默认值都是nil。很容易通过metatables来改变默认值:
function setDefault(t,d)
	local mt = {__index = function () return d end}
	setmetatable(t,mt)
end
tab = {x=10,y=20}
print(tab.x,tab.z)		-- 打印10,nil
setDefault(tab,0)
print(tab.x,tab.z)		--打印10,0

所以说,现在当表关联到mt表时候,无论这个表中缺少什么域,它的__index metamethod被调用并且返回值0。setDefault 函数为每一个需要默认值的表创建了一个新的 metatable。然而metatable有一个默认值d和它本身关联,所以函数不能为所有的表使用单一的一个metatable。为了避免带有不同默认值得所有表使用单一的metatable,我们将每个表的默认值,使用一个唯一的域存储在表的本身里面。如果不担心命名的混乱,可以使用像这样"__"作为我们唯一的域:

local mt = {__index = function (t) return t.__ end}
function setDefault(t,d)
	t.__ = d
	setmetatable(t,mt)
end

如果担心命名混乱,也很容易保证这个特殊的键值唯一性,可以创建一个新表作为键值:

local key = {}
local mt = {__index = function (t) return t[key] end}
function setDefault(t,d)
	t[key] = d
	setmetatable(t,mt)
end

另外一种解决表和默认值关联的方法是使用一个分开的表来处理,在这个特殊的表中索引是表,对应的值为默认值。为了带有不同默认值的表可以重用相同的原表,还有一种解决方法是使用 memoize metatables。这种方法的正确实现需要一种特殊的表:weak table,之后的内容会学习到这种表。

  1. 监控表
    __index 和__newindex 都是只有当表中访问的域不存在时候才起到作用。捕获对一个表的所有访问情况的唯一方法就是保持表为空表。因此试图监控一个表的所有访问情况,应该为真实的表创建一个代理。这个代理是一个空表,并且带有__index 和__newindex metamethods,使用这两个函数来跟踪表所有的访问情况并且指向原始的表。假设t是我们想要跟踪的原始表,可以由以下代码来实现:
t = {}		--原始表
local _t = t 		-- 作一个对原始表的一个私有链接
t = {}			-- 创建一个代理
-- 创建metatable
local mt = {
	__index = function (t,k)
		print("*access to element " .. tostring(k))
		return _t[k]
	end,
	__newindex = function (t,k,v)
		print("*update of element " .. tostring(k) .. " to " .. tostring(v))
		_t[k] = v
	end
}

跟踪的过程如下:

> t[2] = "hello"
*update of element 2 to hello
> print(t[2])
*access to element 2
hello

但是这样子做并不能允许够遍历整个表

如果我们想监控多张表,我们只要将每一个 proxy 和他原始的表关联,所有的 proxy 共享一个公用的metatable 即可。将表和对应的 proxy 关联的一个简单的方法是将原始的表作为 proxy 的域,只要我们保证这个域不用作其他用途。一个简单的保证它不被作他用的方法是创建一个私有的没有他人可以访问的 key。例如下面的一个实例

local index = {}
local mt = {    
	__index = function (t,k)
		print("*access to element " .. tostring(k))
		return t[index][k]
	end,
	__newindex = function (t,k,v)
		print("*update of element " .. tostring(k) .. " to " .. tostring(v))
		t[index][k] = v
	end
}
function track(t)
	local proxy = {}
	proxy[index] = t
	setmetatable(proxy,mt)
	return proxy
end

现在,不管什么时候我们想监控表t,我们要做的只是t= track()

  1. 只读表

采用代理的思想很容易实现一个只读表。我们需要做得只是当我们监控到企图修改
表时候抛出错误。通过__index metamethod,我们可以不使用函数而是用原始表本身来使用表,因为我们不需要监控查寻。这是比较简单并且高效的重定向所有查询到原始表的方法。但是,这种用法要求每一个只读代理有一个单独的新的 metatable,使用__index指向原始表,例如下面的一个例子:

function ReadOnly(t)
	local proxy = {}
	local mt = {
		__index = t,
		__newindex = function(t,k,v)
			error("attempt to update a read-only table",2)
		end
	}
	setmetatable(proxy,mt)
	return proxy
end
days = ReadOnly{"Sunday","Monday","Tuesday","Wednesday", "Thursday","Friday","Saturday"}
print(days[1])		-- Sunday
days[2] = "Noday"		-- error for the data

3.3 数据文件与持久化

我们经常需要序列化一些数据,为了将数据转换为字节流或者字符流,这样我们就可以保存到文件或者通过网络发送出去。我们可以在 Lua 代码中描述序列化的数据,在这种方式下,我们运行读取程序即可从代码中构造出保存的值。
通常,我们使用这样的方式 varname =<exp>来保存一个全局变量的值。
保存不带循环的table
根据表的不同,采取序列化表的方法也有很多,没有一种单一的算法对所有的情况都能很好地解决问题。可以通过以下的函数来对Lua中的变量进行序列化处理:

function serialize(object)
	if type(object) == "number" then
		io.write(object)
	elseif type(object) == "string" then
		io.write(string.format("%q",object))
	elseif type(object) == "table" then
		io.write("{\n")
		for k,v in pairs(object) do
			io.write(" ",k," = ")
			serialize(v)
			io.write(",\n")
		end
		io.write("}\n")
	else
		error("cannot serialize a " .. type(object))
	end
end

尽管它很简单,但是只要表结构是一个树形结构(无循环),它甚至可以处理到嵌套表。但是如果表中有不符合Lua语法的数字关键字或者字符串关键字,上面的代码就有些欠健壮性。所以可以将上面的函数改为下面的形式:

function serialize(object)
	if type(object) == "number" then
  		io.write(object)
	elseif type(object) == "string" then
 		io.write(string.format("%q",object))
	elseif type(object) == "table" then
  		io.write("{\n")
  		for k,v in pairs(object) do
  			-- 这里是修改的地方
  			
  			io.write(" [")
  			serialize(k)
  			io.write("] = ")
  			
  			serialize(v)
   			io.write(",\n")
  		end
  		io.write("}\n")
 	else
		error("cannot serialize a " .. type(object))
	end
end

保存带有循环的table

针对普通拓扑概念上的带有循环表和共享子表的 table,我们需要另外一种不同的方法来处理。构造器不能很好地解决这种情况,我们不使用。为了表示循环我们需要将表名记录下来,下面我们的函数有两个参数:table 和对应的名字。另外,我们还必须记录已经保存过的 table 以防止由于循环而被重复保存。我们使用一个额外的table 来记录保存过的表的轨迹,这个表的下表索引为 table,而值为对应的表名。

function basicSerialize(obj)
	if type(obj) == "number" then
		return tostring(obj)
	else
		-- here we assume the input value as string
		return string.format("%q",obj)
	end
end
function save(name,value,saved)
	-- saved 表示上面提到的记录已经保存的表的踪迹的table
	saved = saved or {}		-- 初始化数值
	io.write(name," = ")
	if type(value) == "number" or type(value) == "string" then
		io.write(basicSerialize(value),"\n")
	elseif type(value) == "table" then
		if saved[value] then		-- 判断数值是否已经被保存
			io.write(saved[value],"\n")
		else
			saved[value] = name		-- 为下一次保存名字
			io.write("{}\n")
			for k,v in pairs(value) do
				local filename = string.format("%s[%s]",name,basicSerialize(k))
				save(filename,v,saved)
			end
		end
	else
		error("Cannot save a " .. type(value))
	end
end

举个例子

a = {x=1,y=2;{3,4,5}}
a[2] = a
a.z = a[1]

调用save(‘a’,a)之后的结果为

a = {}
a[1] = {}
a[1][1] = 3
a[1][2] = 4
a[1][3] = 5
a[2] = a
a["y"] = 2
a["x"] = 1
a["z"] = a[1]

3.4 环境

Lua用一个environment普通的表来保存所有的全局变量,这一结果的优点就是简化了Lua的内部实现。另一主要的优点就是我们可以像其他的表一样操作这个保存全局变量的表。为简化操作,Lua将环境本身存储在一个全局变量_G中。下面是将全局变量的名字全部打印出来:

for name in pairs(_G) do
	print(name)
end

使用动态名字访问全局变量
通常情况下,赋值操作对弈访问和修改全局变量已经足够,但是一些元编程(meta programming)的方式,例如需要操作一个名字被存储在另一个变量中的全局变量,或者在运行时候需要知道的全局变量,通常会写出以下的表达式:

loadstring("value = " .. varname)
-- 或者是
value = loadstring("return " .. varname)
-- 更为简洁的表达式
value = _G[varname]

因为Lua环境只是一个普通的表,所以直接访问这个表即可。
类似的,也可以对一个全局变量赋值操作:_G[varname] = value
对前面的问题概括一下,表域可以是型如"io.read" or "a.b.c.d"的动态名字。我们用循
环解决这个问题,从_G 开始,一个域一个域的遍历:

function getfield(f)
	local v = _G
	for w in string.gfind(f,"[%w_]") do
		v = v[w]
	end
	return v
end

这里我们使用string库中的gfind函数来迭代f中所有的单词(指的是一个或者多个字母下划线的序列)。设置一个域的函数稍微复杂一点,例如

a.b.c.d.e = v
-- 等价于下面的表达式
local temp = a.b.c.d
temp.e = v

即必须记住最后一个名字,必须独立处理最后一个域。下面有这样的一个新的函数setfield函数:

-- 函数setfield当中的域不存在时候还需要创建中间表
function setfield(f,v)
	local t = _G
	for w,d in string.gfind(f,"([%w_]+)(.?)") do
		if d == "." then
			t[w] = t[w] or {}
			t = t[w]
		else
			t[w] = v
		end
	end
end
setfield("t.x.y",10)		-- 创建一个全局变量表t,另一个表t.x,并且对t.x.y赋值为10
print(t.x.y)		-- 打印10
print(getfield("t.x.y"))		-- 打印10

rawset和rawget*
当我们只想单纯的调用table里的字段或者给table字段赋值时候,可以通过rawget函数忽略元表中__index的作用以及通过rawset函数忽略元表中__newindex的作用。举个例子说明这一点:

local days = {
	"Sunday","Monday","Tuesday","Wedensday","Tursday","Friday","Saturday",
	sayHello = function ()
		print("This is a table of weeks!")
	end
}
local temp_meta = {
	__index = days,
	__newindex = function(table,key)
		error("This is ReadOnly table!")
	end
}
tab = {"ExternalDay" = 10}
setmetatable(other_tab,temp_meta)
print(rawget(tab,"HappyDay"))		--打印nil
print(rawget(tab,"ExternalDay"))	--打印10
rawset(tab,"HappyDay","20")
rawset(tab,"sayHello",function()
	print("====================")
end)
print(tab.HappyDay)		--打印20
tab.sayHello()		--打印匿名函数

声明全局变量
由于Lua所有全局变量都保存在一个普通的表当中,所以我们可以使用metatables来改变访问全局变量的行为。
第一个方法实例如下:

-- 下面函数用于声明变量名称
function declare(name,initial)
	rewset(_G,name,initial or false)
end
setmetatable(_G,{
	__newindex = function(_,n)
		error("attempt to write to undeclared variable" .. n,2)
	end
	__index = function(_,n)
		error("attempt to read undeclared variable" .. n,2)
	end
})
-- 这样就可以控制全局变量了
declare "a"
a = 1

但是现在为了测试一个变量是否=存在,并不能简单地比较它是否为nil,如果它是nil访问将会出现错误。所以我们需要的是创建一个辅助表用来保存所有已经声明的变量的名字,不管什么时候metamethod被调用的时候,它会检查这张辅助表看变量是否已经存在。实例如下所示:

local declareNames = {}
function declare(name,initial)
	rawset(_G,name,initial)
	declareNames[name] = true
end
setmetatable(_G,{
	__newindex = function (t,n,v)
		if not declareNames[n] then
			error("attempt to write to undeclared var: " .. n,2)
		else
			rawset(t,n,v)
		end
	end,
	__index = function (t,n)
		if not declareNames[n] then
			error("attempt to read undeclared var: " .. n,2)
		else
			return nil
		end
	end
})

非全局变量的环境
当我们在全局环境中定义变量时候经常会有命名冲突,尤其是在使用一些库的时候,声明变量可能会发生覆盖,这时候就需要一个非全局的环境来解决问题。这里可以使用setfenv函数来该变一个函数的环境:
setfenv(f,table):设置一个函数的环境
(1) 当第一个参数为函数时候,表示设置该函数的环境;
(2) 当第一个参数为一个数字的时候,1表示当前函数,2表示调用自己的函数,3代表调用自己的函数的函数,以此类推
所谓函数的环境,其实一个环境就是一个定义变量和表的表,该函数被限定为只能访问该表中的域,或者在函数体内自己定义的变量。例如:

newfenv = {}
setfenv(1,newfenv)
print(4)		-- 调用print函数失败,因为print并不在当前的环境中

可以这样继承已有的域

a = 10
newfenv = {_G = _G}
setfenv(1,newfenv)
_G.print(1)		-- 打印1
_G.print(_G.a)		-- 打印10
_G.print(a)		--nil新环境中并没有a域

这样,新的环境就可以调用_G了,但是有个弊端,例如每次使用print函数必须手动调用,很不方便。可以使用metatable来解决上述问题:

a = 1
local newfenv = {}
setmetatble(newfenv,{__index = _G})
setfenv(1,newfenv)
print("This is new environment!")

在这段代码新的环境从旧的环境中继承了 print 和 a;然而,任何赋值操作都对新表进行,不用担心误操作修改了全局变量表。依旧可以通过_G修改全局变量:

a = 10
print(a)		-- 打印10
print(_G.a)	-- 打印1
_G.a = 20
print(_G.a)	-- 打印20

更加高级一点的,可以将_G修改为只读表,然后将环境设置为新的环境。

newfenv = {}
setmetatable(_G,{__index = _G,
	__newindex = function(t,n)
		error("aptempt to create a value: " .. n .. " in _G environment!")
	end
})
setmetatable(newfenv,{__index = _G})
setfenv(1,newfenv)
print("This is new environment!")
_G["var"] = 10		-- 抛出异常

小结

本章重点主要在于对table表的熟练操作、metatable和metamethods的熟练操作、Lua环境变换的操作上。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值