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

公良鸿畅
2023-12-01

4.Torch7进阶(三)

这一节主要学习Lua中包的应用以及最重要的面向对象编程处理。在前面一节中,为了处理解决处理表中默认值关联问题,在这一节中的Weak表将会处理这一个问题。另外,这一小节也将会学习到Lua语言中协同程序处理的问题。

4.1 Packages

很多语言中专门提供了某种机制组织全局变量的命名,例如Modula的modules,Java中和Perl中的Packages,C++中namespace。每一种packages机制中都提供了元素的可见性问题和命名冲突问题,每一种程序创建自己的命名空间时候,这样就可以与其他的命名空间中定义的一些名字互不干涉。
Lua中并没有这样的语言机制,但是通过语言机制很容易实现它,像标准库一样,我们这里使用表来描述packages。
基本方法
第一包的简单方法是对包内的每一个对象前面都加上包名作为前缀。例如这样我们定义一个complex来表示复数:

complex = {}
complex.mt = {}     -- metatable for complex
function complex.new(real,image)
	if image == 0 then
		return real
	end
	local num = {real = real,image = image}
	setmetatable(num,complex.mt)
	return num
end
function complex.add(c1,c2)
	if getmetatable(c1) == nil and type(c1) == "number" and getmetatable(c2) == complex.mt then
		return complex.new(c1+c2.real,c2.image)
	elseif getmetatable(c2) == nil and type(c2) == "number" and getmetatable(c1) == complex.mt then
		return complex.new(c2+c1.real,c1.image)
	elseif getmetatable(c1) == complex.mt and getmetatable(c2) == complex.mt then
		return complex.new(c1.real + c2.real,c1.image + c2.image)
	else
		error("error for add two numbers!",2)
	end
end
function complex.sub(c1,c2)
	if getmetatable(c1) == nil and type(c1) == "number" and getmetatable(c2) == complex.mt then
		return complex.new(c1-c2.real,-c2.image)
	elseif getmetatable(c2) == nil and type(c2) == "number" and getmetatable(c1) == complex.mt then
		return complex.new(c1.real - c2,c1.image)
	elseif getmetatable(c1) == complex.mt and getmetatable(c2) == complex.mt then
		return complex.new(c1.real - c2.real,c1.image - c2.image)
	else
		error("error for sub two numbers!",2)
	end
end
function complex.mul(c1,c2)
	if getmetatable(c1) == nil and type(c1) == "number" and getmetatable(c2) == complex.mt then
		return complex.new(c1*c2.real,c1*c2.image)
	elseif getmetatable(c2) == nil and type(c2) == "number" and getmetatable(c1) == complex.mt then
		return complex.new(c1.real*c2,c1.image*c2)
	elseif getmetatable(c1) == complex.mt and getmetatable(c2) == complex.mt then
		local real = c1.real*c2.real - c1.image*c2.image
		local image = c1.image*c2.real + c1.real*c2.image
		return complex.new(real,image)
	else
		error("error for multiply two numbers!",2)
	end
end
function complex.div(c1,c2)
	if getmetatable(c1) == nil and type(c1) == "number" and getmetatable(c2) == complex.mt then
		local real = c1*c2.real/(math.pow(c2.real,2) + math.pow(c2.image,2))
		local image = -c1*c2.image/(math.pow(c2.real,2) + math.pow(c2.image,2))
		return complex.new(real,image)
	elseif getmetatable(c2) == nil and type(c2) == "number" and getmetatable(c1) == complex.mt then
		return complex.new(c1.real/c2,c1.image/c2)
	elseif getmetatable(c1) == complex.mt and getmetatable(c2) == complex.mt then
		local real = (c1.real*c2.real + c1.image*c2.image)/(math.pow(c2.real,2) + math.pow(c2.image,2))
		local image = (c1.image*c2.real - c1.real*c2.image)/(math.pow(c2.real,2) + math.pow(c2.image,2))
		return complex.new(real,image)
	else
		error("error for divide two numbers!",2)
	end
end
function complex.tostring(c)
	if getmetatable(c) == complex.mt then
		if c.real == 0 then
			return tostring(c.image) .. "i"
		end
		if c.image > 0 then
			return tostring(c.real) .. "+" .. tostring(c.image) .. "i"
		elseif c.image == 0 then
			return tostring(c.real)
		else
			return tostring(c.real) .. tostring(c.image) .. "i"
		end
	else
		error("error for the value!",2)
	end
end
function complex.inv(c)
	if getmetatable(c) == complex.mt then
		local n = c.real^2 + c.image^2
		return complex.new(c.real/n,-c.image/n)
	else
		error("error for the number!",2)
	end
end
complex.mt.__add = complex.add
complex.mt.__sub = complex.sub
complex.mt.__mul = complex.mul
complex.mt.__div = complex.div
complex.mt.__tostring = complex.tostring
return complex

这个库定义了一个全局的名字:complex。其他的定义都是放置于这个表内部。
但是这种使用表来实现的包和真正的包的功能并不是完全相同。首先,我们对每一个函数定义都必须显式地在前面加上包的名称。第二,同一包内的函数互相调用必须在被调用函数前指定包名。改进这个问题是这样的,我们可以使用固定的局部变量名,然后将这个局部变量赋值给最终的包。

local Pcomplex = {}
complex = Pcomplex		-- 包的名字
-- 然后将复数的定义放置于Pcomplex中
...
-- 这个语句并非必须的
return complex

将上述文件命名为complex.lua,并将其放在工作目录下,使用的时候直接可以require(“complex”)即可以使用这个包

require("complex")
a = complex.new(1,2)
b = complex.new(3,4)
print("sub:",a-b)
print("add:",a+b)
print("mul:",a*b)
print("div:",a/b)
print(a + 6, 6+ a)
print(a - 6,6-a)
print(a *6,6*a)
print(a/6,6/a)
c = complex.new(0,5)
print(c)

私有成员
有时候,packages公开它所有的内容,即任何packages客户端都能够访问到它。当然,packages也需要拥有自己的私有部分。Lua中的一个传统的办法就是将其定义为局部变量来实现。例如将上面的例子修改如下

local Pcomplex={}
complex = Pcomplex
local function checkComplex(c)
	if not ((type(c) == "table") and tonumber(c.real) and tonumber(c.image)) then
		error("bad complex number",3)
	end
end
function Pcomplex.add(c1,c2)
	checkComplex(c1)
	checkComplex(c2)
	return Pcomplex.new(c1.real+c2.real,c1.image+c2.image)
end
...

package 中所有的名字都在一个独立的命名空间中。Package 中的每一个实体(entity)都清楚地标记为公有还是私有。另外,我们实现一个真正的隐私(privacy):私有实体在 package 外部是不可访问的。缺点是访问同一个package 内的其他公有的实体写法冗余,必须加上前缀 Pcomplex.。还有一个大的问题是,当我们修改函数的状态(公有变成私有或者私有变成公有)我们必须修改函数得调用方式。
我们可以这样修改,可以将package内部所有的函数都声明为局部的,最后将它们放在最终的表中。例如我们可以这样的定义:

local function checkComplex(c)
	if not ((type(c) == "table") and tonumber(c.real) and tonumber(c.image)) then
  		error("bad complex number",3)
 	end
end
local function new(r,i) return {real=r,image=i} end
local function add(c1,c2)
	checkComplex(c1)
	checkComplex(c2)
	return new(c1.real+c2.real,c1.image+c2.image)
end
...
complex = {
	new = new,
	add = add,
	sub = sub,
	mul = mul,
	div = div,
}

现在我们不再需要调用函数的时候在前面加上前缀,公有的和私有的函数调用方法相同。
包与文件

我们经常写一个 package 然后将所有的代码放到一个单独的文件中。然后我们只需
要执行这个文件即加载 package。例如,如果我们将上面我们的复数的 package 代码放到一个文件 complex.lua 中,命令require (“complex”)将打开这个 package。require 命令不会将相同的 package 加载多次。
需要注意的问题是,搞清楚保存 package 的文件名和 package 名的关系。当然,将
他们联系起来是一个好的想法,因为 require 命令使用文件而不是 packages。一种解决方法是在 package 的后面加上后缀(比如.lua)来命名文件。Lua 并不需要固定的扩展名,而是由你的路径设置决定。
在 Lua 中我们使用_REQUIREDNAME 变量来重命名。当require加载一个文件的时候,它定义了一个变量表示虚拟的文件名。因此,在package中可以这样写:

local P = {}		-- package
if _REQUIRENAME == nil then
	complex = P
else	
	_G[_REQUIRENAME] = P
end

代码中的 if 测试使得我们可以不需要 require 就可以使用 package。如果_REQUIREDNAME 没有定义,我们用固定的名字表示 package。
使用全局表
我们可以将所有的共有函数声明为全局变量,然后让它们自动作为独立的表存在,所有package必须要做到的是将这个表注册为package的名字。举个例子:

local P = {}
complex = P
setfenv(1,P)
-- 下面的函数add会自动变成complex.add
function add(c1,c2)
	return new(c1.real+c2.real,c1.image + c2.image)
end

这个package不需要前缀调用其他函数。这种方法提供了对 package 很好的支持:程
序员几乎不需要做什么额外的工作,调用同一个 package 内的函数不需要前缀,调用公有和私有函数也没什么区别。如果程序员忘记了 local 关键字,也不会污染全局命名空间,只不过使得私有函数变成公有函数而已。另外,我们可以将这种技术和前一节我们使用的 package 名的方法组合起来:

local P = {}		-- package
if _REQUIRENAME == nil then
	complex = P
else 
 	_G[_REQUIRENAME] = P
end

但是这样的结果会使得失去了访问所有以前的全局变量。有以下几种修改的办法:
方法一:使用继承

local P = {}
setmetatable(P,{__index = _G})
setfenv(1,P)

使用这样的一个结构就可以直接访问所有的全局标识符,但是也会带来另外的一个结果,即package现在包含了所有的全局变量。
方法二:声明一个局部变量来保存旧有的环境

local P = {}
pack = P
local _G = _G
setfenv(1,P)

所以现在对外部的访问必须加上前缀_G,但是访问速度更快。
比较正规一点的方法:将需要用到的函数或者packages声明为local

local P = {}
pack = P
-- Import Section
-- declare everything this package from outside
local sqrt = math.sqrt
local io = io
setfenv(1,P)

4.2 面向对象编程处理

Lua中的表不仅在某种意义上是一种对象。像对象一样,表也会有状态(成员变量);也有与对象的值独立的本性,特别是拥有两个不同值得对象代表两个不同的对象;一个对象在不同时候也可以有不同的值,但它始终是一个对象;与对象类似,表的声明周期预期在什么时候创建、在哪里创建没有关系。对象有它们的成员函数,表也会有。举个例子

Account = {balance = 0}
function Account.withdraw(v)
	Account.balance = Account.balance - v
end
-- 调用成员函数
Account.withdraw(100.0)

然而,在一个函数内部使用全局变量名Account并不是一个非常好的习惯。第一,这个函数只能在这个特殊的对象;第二,即使对这个特殊的对象而言,这个函数只能够在对象被存储在特殊的变量中才可以使用,改变了名字之后并不能使用。例如

a = Account ; Account = nil
a.withdraw(100.00)		--出现错误

一个灵活的方法就是,定义方法的时候带上一个额外的参数,来表示方法作用的对象。这个参数常常称作为this指正或者是self指针:

function Account.withdraw(self,v)
	self.balance = self.balance - v
end
-- 并不需要制定它的操作对象了
a1 = Account;Account = nil
...
a1.withdraw(a1,100.0)

将函数作用于多个对象之上:

a2 = {balance = 0, withdraw = Account.withdraw}
a2.withdraw(a2,260.0)

self参数的使用时很多面向对象语言的要点。大多数OO语言将这种机制隐藏起来,这样就不必要声明这个参数。Lua中通过使用冒号操作符来隐藏这个声明。 举个例子

function Account:withdraw(v)
	self.balance = self.balance - v
end
-- 调用方法
a:withdraw(100.0)

其实上述的定义方法等同于定义含有self参数的函数。所以我们只需要处理好相对应的参数即可:

Account = {
	balance = 0,
	withdraw = function (self,v)
		self.balance = self.balance - v
	end
}
function Account:deposit(v)
	self.balance = self.balance + v
end
Account.deposit(Account,200.0)

所以引发了这样的一个问题:缺少一个class系统,继承和隐藏,下面会解决这个问题。
类的基本定义
Lua中其实并不存在类的概念,每一个对象定义它自己的行为并拥有自己的形状。这种基于原型(prototype)语言效仿类并不难,这种语言中对象没有类,然而每一个对象都有一个prototype,当调用不属于对象的某些操作的时候,会最先用到prototype中查找这些操作。这样中原型语言中实现类是这样的,我们创建一个原型对象作为类,其他对象为类的实例。例如,如果有两个对象a和b,让b作为a的prototype只需用metatable实现即可:

setmetatable(a,{__index = b})

这样a中不存在的成员都会从b中查找,可以将b看作是类,a看作是对象。举个例子

Account = {balance = 0}
function Account:new(obj)
	obj = obj or {}
	setmetatable(obj,self)
	self.__index = self
	return obj
end
function Account:deposit(v)
	self.balance = self.balance + v
end
a = Account:new{balance = 0}
a:deposit(100.0)

然而,调用a:deposit(100.0)的时候,实际上调用的是a.deposit(a,100.0),冒号的表示其实是语法上的便利性。然而Lua在表a中找不到deposit函数,此时回到metatable的__index中对应的表中查找,情况如下表所示:

getmetatable(a).__index.deposit(a,100.0)
-- 等价于以下的代码
Account.deposit(a,100.0)

Lua传递a作为self参数调用原始的deposit函数。
继承
假设拥有一个基类Account,派生类SpecialAccounty允许客户取款超过它的存款额限制,继承Account,如下面所示

Account = {balance = 0}
function Account:new(obj)
	obj = obj or {}
	setmetatable(obj,self)
	self.__index = self
	return obj
end
function Account:deposit(v)
	self.balace = self.balance + v
end
function Account:withdraw(v)
	if v > self.balance then
		error("insufficient funds")
	end
	self.balance = self.balance - v
end
-- SpecialAccount仅仅是Account的一个实例
SpecialAccount = Account:new()
s =  SpecialAccount:new{limit = 1000.0}

SpecialAccount 从Account中继承了new方法,当执行new函数的时候,self也便指向了SpecialAccount。故而,s的metatable是SpecialAccount,__index也是SpecialAccount。这样s继承了SpecialAccount,后者继承了Account。但是下面的例子会有以下的调用方法s:deposit(100.0)是从父类Account中查找并调用。当然,也可以重写父类中的方法:

function SpecialAccount:getLimit()
	return self.limit or 0
end
function SpecialAccount:withdraw(v)
	if v - self.balance >= self:getLimit() then
		error("insuficient funds")
	end
	self.balance = self.balance - v
end

现在当调用方法s:widthdraw(200.0)时候,Lua会直接调用新定义在SpecialAccount中的函数。
Lua中还有一个新的特性就是,不必要创建一个新的类然后去定义一个新的行为,直接就可以在对象中定义和实现。例如

function s:getLimt()
	return self.balance * 0.10
end

多重继承
多继承实现的关键在于,将函数作用于__index。记住,当一个表的metatable存在一个__index函数时候,如果Lua调用一个原始表不存的在函数,Lua就会调用__index 指定的函数。这样可以用__index 实现在多个父类中查找子类不存在的域。
多重继承意味着一个类拥有多个父类,所以我们这里定义一个特殊的函数createClass 来完成创建子类的过程,将创建的新类的父类作为这个函数的参数。这个函数主要是设计一个表来表示新类,并且将它的metatable设定为一个可以实现多继承的__index metamethod 。举个例子

local function search(k,plist)
	for i=1,table.getn(plist) do
		local v = plist[i][k]		--找到第i个父类
		if v then return v end
	end
end
function createClass(...)
	local c = {}
	setmetatable(c,{__index = function(t,k)
		return search(k,arg)
	end})
	c.__index = c
	function c:new(obj)
		obj = obj or {}
		setmetatable(obj,c)
		return obj
	end
	return obj
end
-- 假定类Named和前面的Account类
Named = {}
function Named:getname()
	return self.name
end
function Named:setname(n)
	self.name = n
end
-- 继承两个类
NamedAccount = createClass(Account,Named)
-- 创建实例并调用成员函数
account = NamedAccount:new(name = "Paul")
print(account:getname())		--打印Paul

其中最后一句话中查找过程是这样的:首先,account中找不到getname,然后去查找account的metatable的__index,即NamedAccount,但是NamedAccount也没有getname,因此Lua查找NamedAccount的metatable的__index,因为这个域中包含有一个函数,Lua首先到Account中寻找getname,但是没有,然后再Named中找到并返回查找结果。这种方法在多重继承中搜索很复杂,使得程序效率变低。一个简单的改善性能的方法是将继承方法拷贝到子类当中去。如下方法:

...
setmetatable(c,{__index = function (t,k)
	local v = search(k,arg)
	t[k] = v		-- 保存下一次的搜索结果
	return v
end})
...

私有性
Lua中的主要对象设计并没有涉及到私有访问机制。但是可以使用不同方式来实现这种私有性。基本的设计思想是,每个对象用两个表来表示:一个描述状态;另一个描述操作(或者称作接口)。对象本身通过第二表来访问,即通过接口访问。为了避免为授权的访问,表示状态的表中不涉及到操作;表示操作的表也不涉及到状态,取而代之的是,状态被保存在方法的闭包内。例如使用函数工厂创建新的对象:

function newAccount(initialBalance)
	local self = {balance = initialBalance}
	local withdraw = function (v)
		self.balance = self.balance - v
	end
	local deposit = function (v)
		self.balance = self.balance + v
	end
	local getBalance = function ()
		return self.balance
	end
	return {
		withdraw = withdraw
		deposit = deposit
		getBalance = getBalance
	}
end

这里关键点在于:这些方法没有使用到额外的参数self,代替的是直接访问self,故而不能使用冒号访问这些对象的成员函数,只能像其他函数一样调用:

acc1 = newAccount(100.0)
acc1.withdraw(40.0)
print(acc1.getBalance())

这样,并没有什么方法直接访问对象,只能通过定义的一些函数来访问它。实际上,还可以将一些函数放置到这些私有表当中。这样实现

function newAccount(initialBalance)
	local self = {
		balance = initialBalance,
		LIM = 10000.0
	}
	local extra = function ()
		if self.balance > self.LIM then
			return self.balance * 0.10
		else
			return 0
		end
	end
	local getBalance = function ()
		return self.balance + self.extra()
	end
	...
end

这样,用户并不能够直接访问extra函数了。
Single-Method的对象实现方法
OO设计的方法有一种特殊情况:对象只有单一的方法。这种情况下,并不需要创建一个接口表,取而代之的是,我们将这个单一的方法作为对象返回。举个例子

function newObject(value)
	return function (action,v)
		if action == "get" then return value
		elseif action == "set" then value = v
		else error("invaild action")
	end
end
d = newObject(0)
print(d("get"))
d("set",10)
print(d("get"))

这样的对象使用一个单独的闭包,代价比起表来小很多。这种方法没有继承但是有私有性:访问对象状态唯一的方式就是通过它的内部的方法。

4.3 协同程序

协同程序与多线程情况下的线程比较类似:有自己的堆栈,自己的局部变量,有自己的指令指针,但是和其他协同程序共享全局变量等等很多信息。线程与协同程序不一样的地方在于:在多处理器情况下,从概念上来说多线程程序同时运行多个程序;然而协同程序是通过协作来完成,在任一指定的时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。
基本语法

方法描述
coroutine.create()创建coroutine,返回coroutine,参数是一个函数,当和resume配合使用的时候就会唤醒函数调用。
coroutine.resume()重启coroutine,和create配合使用
coroutine.yield()可以将正在运行的coroutine挂起,将coroutine设置为挂起状态,这个和resume配合使用能有多种效果
coroutine.status()查看coroutine状态信息
coroutine.wrap()创建coroutine,返回一个函数,一旦调用这个函数,就进入coroutine,和create功能重复,但是会返回一个函数
coroutine.running()返回正在运行的coroutine,一个coroutine就是一个线程,当使用running的时候,就是返回一个coroutine的线程号。

协同有三个基本状态:挂起状态、运行状态、停止状态。协同程序开始的状态为挂起状态。下面举个例子

co = coroutine.create(function ()
	print("Hello")
end)
print(type(co))		-- 打印thread
print(coroutine.status(co))		--打印suspended
coroutine.resume(co)	-- 将挂起状态变为运行状态
print(coroutine.status(co))		-- 打印dead

yield函数可以将正在运行的程序挂起,例如

coroutine.create(function ()
	for i=1,10 do
		print("co",i)
	end
	coroutine.yeild()
end)
coroutine.resume(co)		-- 打印co	1
print(coroutine.status(co))		-- 打印suspended
--每次调用coroutine.resume(co)运行到yield函数处就会将程序挂起

当调用一个已经结束的coroutine时候,resume就会返回false和错误信息:

false	cannot resume dead coroutine

可以返回参数或者传递参数信息:

co1 = coroutine.create(function (a,b,c)
	print("co",a,b,c)
end)
coroutine.resume(co1,1,2,3)	--传入参数信息
co2 = coroutine.create(function (a,b)
	coroutine.yield(a+b,a-b)
end)
print(coroutine.resume(co2,10,20))		--打印true 30 10
co3 = coroutine.create(function ()
	return "Hello"
end)
print(coroutine.resume(co3))		--打印true		Hello
co4 = coroutine.create(function ()
	print("co",coroutine.yield())
end)
coroutine.resume(co4)	--打印co
coroutine.resume(co4,1,2,3)	--打印co 1 2 3

注意:Lua中提供的这种协同称为不对称协同,即挂起一个正在执行的协同函数与使一个被挂起的协同再次执行的函数是不相同的。
管道与过滤器
协同最有代表的问题解决是消费者与生产者问题。假定函数producer()不断产生数据送给消费者,consumer()不断消费数据,关键问题是如何使得消费者的receive和生产者的send如何协同工作。在Lua中,调用者与被调用者之间的resume-yield关系不会颠倒。当一个协同调用yield时候并不会进入一个新的函数,取而代之的是返回一个未决的resume调用;调用resume的时候也不会开始一个新的函数而是返回yield的调用。我们在开始的时候调用消费者,当消费者需要值得时候唤起生产者生产值,生产者生产值之后停止直到消费者再次请求,即消费者驱动的设计。为了更加形象化表述,我们添加了加工者filter,即生产者生产的数据被加工者进行加工,然后消费者进行消费,这样的一个协同程序。例子如下

require("socket")
-- 随机生成数字字母字符串
function randomAlpha(num)
	local key = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
	local str = ""
	local x = 1
	math.randomseed(tonumber(tostring(os.time()):reverse():sub(1,9)))
	for k=1,num do
		x = math.random(1,#key)
		str = str .. string.sub(key,x,x)
	end
	return str
end
function producer()
	return coroutine.create(function ()
		while true do
			local str_line = randomAlpha(10)
			send(str_line)
			socket.select(nil,nil,0.1)		--为了防止生产数据过快,这里使用到了延迟函数
		end
	end)
end
function filter(newproduct)
	return coroutine.create(function()
			local index = 1
			while true do
				local str_line = receive(newproduct)
				str_line = string.format("%5d %s",index,str_line)
				send(str_line)
				index = index + 1
			end
		end
	end)
end
function consumer(newproduct)
	while true do
		local value = receive(newproduct)
		print(value)
	end
end
function receive(newproduct)
	local status,value = coroutine.resume(newproduct)
	return value
end
function send(x)
	coroutine.yield(x)
end
-- 进行生产者、加工者、消费者之间的协作
p = producer()
f = filter(p)
consumer()

自然地联系到UNIX中管道,协同是一种非抢占式多线程处理模式。管道的方式下,每一个任务在独立的进程中运行,而在协同方式下,每一个任务运行在独立的协同代码中。管道在读(consumer)与写(producer)之间提供了一个缓冲,这在上下文管道中是非常重要的,因为在进程间切换代价是很高的。
用作迭代器的协同处理
循环的迭代器可以是看做生产者-消费者模式的特殊的例子。迭代函数产生值给循环消费,因而可以使用协同来实现迭代器。协同的一个关键的特征就是可以颠倒调用者与被调用者之间的关系。这样可以使用它实现一个迭代器而不保存迭代函数返回的状态。举个例子

function randomTable(m,n,num)
	-- 生成长度为num且元素包含在区间[m,n]的表
	math.readomseed(tonumber(tostring(os.time()):reverse():sub(1,9)))
	local t = {}
	for k = 1.num do
		t[k] = math.random(m,n)
	end
	return t
end
-- 生成具体的排列组合数字
function permgen(a,n)
	if n == 0 then
		printResult(a)
	else
		for i = 1,n do
			a[n] , a[i] = a[i],a[n]
			permgen(a,n-1)
			a[n] , a[i] = a[i],a[n]
		end
	end
end
function printResult(a)
	local str_line = ""
	for i,v in ipairs(a) do
		str_line = tostring(v) .. "," .. str_line
	end
	print(str_line)
end
t = randomTable(10,99,3)
perngen(t,3)

有了上面的生成器之后,我们修改其为迭代函数
第一步,将printResult修改为yield

function permgen(a,n)
	if n == 0 then
		coroutine.yield(a)
	else
		...
	end
end

第二步,定义一个迭代工厂,修改生成器在生成器内创建迭代函数,并使得生成器运行在一个协同程序内,迭代函数负责请求协同产生下一个可能的序列。

function perm(a)
	local n = table.getn(a)
	local co = coroutine.create(function()
		permgen(a,n)
	end)
	return function ()
		local code,res = coroutine.resume(co)
		return res
	end
end
t = randomTable(10,99,3)
for p in perm(t) do
	printResult(p)
end

perm函数将对协同的resume的调用封装在一个函数内部,coroutine提供了一个这样的函数coroutine.wrap:wrap创建了一个函数,warp返回一个函数,当这个函数被调用的时候将resume协同。wrap中resume协同的时候不会返回错误代码作为第一个结果,当发生错误的时候将会抛出错误,因此可以这样修改程序

function perm(a)
	local n = table.getn(a)
	return coroutine.wrap(function()
		permgen(a,n)
	end)
end

4.4 Weak表

Lua自动进行内存的管理,程序只能创建对象(表,函数等等),而没有执行删除对象的函数。通过使用垃圾收集技术,Lua会自动删除那些失效的对象。和其他的不同,Lua的垃圾收集器不存在循环的问题。垃圾收集器只能在确认对象失效之后才会进行收集;但是它不会知道对垃圾的定义。例如任何在全局变量中声明的对象,都不是Lua认为的垃圾,即便是在程序中并没有用到它。
Weak表是一种用来告诉Lua一个引用不应该防止对象被收回的机制。一个weak引用是指一个不被Lua认为是垃圾的对象的引用。如果一个对象所有的引用指向都是weak,对象将被收集,而那些weak引用会被删除。Lua通过weak tables来实现weak引用:一个weak tables是指所有引用都是weak的table,这意味着如果一个对象只存在于weak tables中,Lua将会最终收集。

表有 keys 和 values,而这两者都可能包含任何类型的对象。在一般情况下,垃圾收
集器并不会收集作为 keys 和 values 属性的对象。也就是说,keys 和 values 都属于强引用,他们可以防止他们指向的对象被回收。在一个 weak tables 中,keys 和 vaules 也可能是 weak 的。那意味着这里存在三种类型的 weak tables:weak keys 组成的 tables;weak values 组成的 tables;以及纯 weak tables 类型,他们的 keys 和 values 都是 weak 的。与table 本身的类型无关,当一个 keys 或者 vaule 被收集时,整个的入口(entry)都将从这个 table 中消失。

表的 weak 性由他的 metatable 的__mode 域来指定的。在这个域存在的时候,必须是个字符串:如果这个字符串包含小写字母‘k’,这个 table 中的 keys 就是 weak 的;如果这个字符串包含小写字母‘v’,这个 table 中的 vaules 就是 weak 的。
举个例子:

weakTable = {}
weakTable[1] = function ()
	print("I am the first element.")
end
weakTable[2] = function ()
	print("I am the second element.")
end
setmetatable(weakTable,{__mode = "v"})
print(table.getn(weakTable))		--打印为2
ele = weakTable[1]			-- 指定引用
collectgarbage()
print(table.getn(weakTable))		--打印为1
ele = nil			-- 不再引用
collectgarbage()		--	打印为0
print(table.getn(weakTable))

记忆函数
一个相当普遍的编程技术是用空间换取时间,可以通过记忆函数结果来进行优化,当用同样的参数再次调用函数的时候,它可以自动返回记忆的结果。例如用一个表来存储一些调用的执行字符串:

results = {}
function mem_loadstring(s)
	if results[s] then
		return results[s]
	else
		local res = loadstring(s)
		results[s] = res
		return res
	end
end

这样做的结果会消耗大量的存储,造成数据信息的冗余结果。利用weak table可以解决这个问题:如果这个结果表中有weak值,每次的垃圾收集循环都会移除当前时间内所有未被使用的结果:

local results = {}
setmetatable(results,{__mode = "v"})
function mem_loadstring(s)
	if results[s] then
		return results[s]
	else
		local res = loadstring(s)
		results[s] = res
		return res
	end
end

事实上,由于table的索引下标经常是字符串式的,可以将table全部设置为weak:

setmetatable(results,{__mode={"kv"}})

记忆技术在保持一些类型对象的唯一性上同样有用。例如下面的一个例子:

local results = {}
setmetatable(results,{__mode = "v"})
function createRGB()
	local key = r .. "-" .. g .. "-" .. b
	if results[key] then
		return results[key]
	else
		
	end
	
end

关联对象属性
weak table的另外一个重要的应用就是和对象的属性关联。当对象是表的时候,可以使用一个合适的唯一key来将属性保存在表中,简单的方法,新建一个对象然后把它当成key使用。然而,如果对象不是 table,它就不能自己保存自身的属性。即使是 tables,有些时候我们可能也不想把属性保存在原来的对象中去。例如,我们可能希望将属性作为私有的,或者我们不想在访问 table 中元素的时候受到这个额外的属性的干扰。在上述这些情况下,我们需要一个替代的方法来将属性和对象联系起来。当然,一个外部的 table 提供了一种理想化的方式来联系属性和对象(tables 有时被称作联合数组并不偶然)。我们把这个对象当作 key 来使用,他们的属性作为 vaule。一个外部的 table 可以保存任何类型对象的属性(就像 Lua 允许我们将任何对象看作key)。此外,保存在一个外部 table 的属性不会妨碍到其他的对象,并且可以像这个 table本身一样私有化。
重述带有默认值的表
之前在创建含有默认值的表的处理时候提及到两种处理办法,这两种办法其实就是来源于通用技术的特殊应用:对象属性和记忆。
第一种解决方法:我们使用weak table来将默认values和每一个table相联系:

local defaults = {}
setmetatable(defaults,{__mode = "k"})
local mt = {__index = function(t)
	return defaults[t]
end}
function setDefault(t,d)
	defaults[t] = d
	setmetatable(t,mt)
end

如果没有weak 的key,它就会将所有的带有默认值的tables设定为永久存在。
第二种解决方法:我们使用不同的metatable来保存不同的默认值,但是当我们重复使用一个默认值的时候,重用一个相同的metatable。这是一个典型记忆技术的应用:

local metas = {}
setmetatable(metas,{__mode = "v"})
function setDefault(t,d)
	local mt = metas[d]
	if mt == nil then
		mt = {__index = function()
			return d
		end}
		metas[d] = mt  -- memoize
	end
	setmetatable(t,mt)
end

这种情况下,我们使用 weak vaules,允许将不会被使用的 metatables 可以被回收。

小结

这一节最重要的内容是面向对象编程的一些内容,这个设计到了Torch使用的核心的一些内容,其后包括一些weak table 使用方法、Lua编程环境的处理方式和方法、包的使用方法以及在多线程处理过程中协同程序的操作方式和方法。接下来会陆续介绍Torch的使用方法以及Lua语言中更多的操作和使用,例如标准库、网络编程(包含有并发处理)、C API函数的应用和后端脚本编程等等。

 类似资料: