Loading

Lua笔记三

1.数据结构

table是Lua中唯一的数据结构,其他语言所提供的其他数据结构比如:arrays、records、lists、queues、sets等,Lua中不仅可以用table完成同样的功能,而且table的功能更加强大。

1.1数据

在Lua中通过整数下标访问表中的元素即可简单的实现数组。并且数组不必视像指定大小,大小可以随需要动态的增长。

通常我们初始化数组的时候就间接的定义了数组的大小,

a = {}		--new array
for i=1,1000 do
	a[i]=0
end

通过初始化,数组a的大小已经确定为1000,企图访问1-1000以外的下标对应的值将返回nil。你可以根据需要的定义数组的下标从0,1或者任意其他的数值开始。

-- creates an array with indices from -5 to 5
a = {}
for i=-5,5 do
	a[i]=0
end

然而在Lua中习惯上数组的下标从1开始.Lua的标准库与此习惯保持一致。

我们可以使用构造器在创建数组的同时并初始化数组。

1.2阵和多维数组

Lua中主要有两种表示矩阵的方法,第一种是用数组的数组表示。也就是说一个表的元素是另一个表。如下面代码创建一个n行m列的矩阵:

mt={}		--create the matrix
for i=1,N do
	mt[i]={}	--create a new row
	for j=1,M do
		mt[i][j]=0
	end
end

由于Lua中table是个对象,所以对于每一行我们必须显式的创建一个table。

第二中表示矩阵的方法是将行和列组合起来,如果索引下标都是整数,通过第一个索引乘于一个常量(列)再加上第二个索引,看下面的例子实现创建n行m列的矩阵:

mt={}			--create the matrix
for i=1,N do
	for j=1,M do
		mt[i*M+j]=0
	end
end

如果索引是字符串的话,可以用一个单字符将两个字符串索引连接起来构成一个单一的索引下标,

1.3链表

Lua中用tables很容易实现链表,每一个结点是一个table,指针是这个表的一个域,并且指向另一个节点(table)。例如,要实现一个只有两个域:值和指针的基本链表,代码如下:

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

1.4队列和双端队列

虽然可以使用Lua的table库提供的insert和remove操作来实现队列,但这种方式实现的队列针对大数据量时效率太低,有效的方式是使用两个索引下标,一个表示一个元素,另一个表示最后一个元素。

function ListNew()
	return {first = 0,last = -1}
end

为了避免污染全局命名空间,我们重写上面的代码,将其放在名为list的table中

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				--to allow garbage colloection
    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 -- to allow garbage collection
	list.last = last - 1
	return value
end	

对严格意义上的队列来讲,我们只能调用pushright和popleft,这样以来,first和last的索引值都随之增加,幸运的是我们可以使用的lua的table实现的,你可以访问数据的元素,通过使用下标从1到20,也可以16777216到16777236。另外,Lua使用双精度表示数字,假定你每秒钟执行100万次插入操作,在数值溢出以前你的程序可以运行200年。

1.5集合和包

假定你想列出在一段源代码中出现的所有标示符。一些C程序员喜欢用一个字符串数组来表示,讲所有的保留字放在数组中,对每一个标示符到这个数组中查找看是否为保留字,有时候为了提高查询效率,对数组存储的时候使用二分查找或者hash算法。

Lua中表示这个集合有以一个简单有效的办法,讲所有集合中的元素作为下标存放在一个table里,下面不需要查找table,只需要测试看对于给定的元素,表的对应下标的元素值是否为nil。

reserved = {
	["while"] = true ,   ["end"] = true,
	["function"] = true, ["local"] = true
}

for w in allwords() do
	if reserved[w] then
	-- 'w' is a reserved word
	...

还可以使用辅助函数更加清晰的构造集合:

function Set(list)
	local set={}
	for _,l in ipairs(list) do set[l]=true end
	return set
end

1.6字符串缓冲

假定你要拼接很多个小的字符串为一个大的字符串,比如,从一个文件中逐行读入字符串。你可能写出下面的这样的代码:

-- WARNING:bad code ahead!
loacl buff=""
for line in io.line() do
	buff=buff..line.."\n"
end

尽管这段代码上山去很正常,但在Lua中它的效率极低。因为Lua采用了垃圾收集算法,与其他采用垃圾收集算法的并且字符串不可变的语言都存在这个问题。Java示最著名的例子,Java专门提供StringBuffer来改善这种情况。

我们最初的算法通过将循环每一行的字符串连接到老串上来解决问题,新的算法避免如此:它连接两个小串成为一个稍微大的串,然后连接稍微大的串成更大的串。。。算法的核心是:用一个栈,在栈的底部用来保存已经生成的大的字符串,而小的串从栈定入栈。栈的状态变化和经典的汉诺塔问题类似:位于栈下面的串肯定比上面的长,只要一个较长的串入栈后比它下面的串长,就将两个串合并成一个新的更大的串,新生成的串继续与相邻的串比较如果长于底部的将继续进行合并,循环进行到没有串可以合并或者到达栈底。

function newStack()
	return {""}			--starts with an empty string
end

function addString(stack ,s)
	table.insert(stack,s)		--push 's' into the stack
	for i=#stack-1,l,-1 do
		if string.len(stack[i])>strng.len(stack[i+1]) then
			break
		end
	stack[i]=stack[i]..table.remove(stack)
	end
end

要想获取最终的字符串,我们只需要从上向下一次合并所有的字符串即可。table.concat 函数可以将一个列表的所有串合并。

使用这个新的数据结构,我们重写我们的代码:

local s=newStack()
for line in io.lines() do
	addString(s,line.."\n")
end
s=toString(s)

最终的程序读取 350 KB 的文件只需要 0.5s,当然调用 io.read("*all")仍然是最快的只需要 0.02s

2面向对象程序设计

表有状态(成员变量);也有与对象的值独立的本性,特别是拥有两个不同值得对象(table)代表两个不同得对象。对象有他们得成员函数,表也有:

Account = {balance=0}
function Account.withdraw(v)
	Account.balance = Accout.balance - v
end

这个定义创建一个新的函数,并且保存在Account对象的withdraw域内,下面我们可以这样调用:

Account.withdraw(100.00)

这个函数就是我们所谓的方法,然而,在一个函数内部使用全局变量名Account是一个不好的习惯。

首先,这个函数只能在这个特殊的对象中使用;第二,即使对这个特殊的对象而言,这个函数也只有在对象被存储在特殊的变量中才能使用。如果我们改变了这个对象的名字,函数wirhdraw讲不能工作。

这种行为违背了前面的对象应该有独立的什么周期的原则。

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

function Account.withdraw(self,v)
	self.balance=self.balance-v
end

现在,当我们调用这个方法的时候不需要指定他操作的对象了:

a1=Account;Account=nil
...
al.withdraw(a1,100.00)

使用self参数定义函数后,我们可以讲这个函数用于多个对象上:

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

self参数的使用是很多面向对象语言的要点。大多数OO语言将这种机制隐藏起来,这样程序员不必声明这个参数。Lua也提供了通过使用冒号操作符来隐藏这个参数的声明。我们可以重写上面的代码:

function Account:withdraw(v)
	self.balance=self.balance-v
end

冒号的效果相当于在函数定义和函数调用的时候,增加一个额外的隐藏参数。这种方式只是提供了一种方便的语法,实际上并没有什么新的内容。我们可以使用dot语法定义函数而用冒号语法调用函数,反之亦然,只要我峨嵋你正确的处理好额外的参数。

2.1类

Lua中不存在类的概念,每个对象定义他自己的行为并拥有自己的形状(shape)。每个对象都有一个prototype(原型),当调用不属于对象的某些操作时,会最先会到prototype中查找这些操作。在这类语言实现类(class)的机制,我们创建一个对象,作为其他对象的原型即可(原型对象为类,其他对象为类的instance)。类与prototype的工作机制相同,都是定义了特定对象的行为。

在Lua中,使用前面章节我们介绍过的继承的思想,很容易实现prototypes。更明确的来说,如果我们有两个对象a和b,我们想让b作为a的prototype只需要:

setmetatable(a,{_index=b})

这样,对象a调用任何不存在的成员都回到对象b中查找。术语上,可以将b看作类,a看作对象。

2.2继承

通常面向对象语言中,继承使得类可以访问其他类的方法,这在Lua中也很容易实现:

假设我们有一个基类Account:

Account = {balance = 0}
function Account:new(o)
	o = o or {}
	setmetatable(o, self)
	self.__index=self
	return o
end

function Account:deposit (v)
	self.balance = self.balance + v
end

function Account:withdraw(v)
	if v > self.balance then error"insufficient funds" end
	self.balance = self.balance - v
end

我们打算从基类派生出一个子类SpecialAccount,这个子类允许客户取款超过它的存款余额限制,我们从一个空类开始,从基类继承所有操作:

SpecialAccont = Account:new()

到现在为止,SpecialAccount仅仅是Account的一个实例。现在奇妙的事情发生了:

s=SpecialAccount:new{limit=1000.00}

SpecialAccount 从 Account 继承了 new 方法,当 new 执行的时候,self 参数指向SpecialAccount。所以,s 的 metatable 是 SpecialAccount,__index 也是 SpecialAccount。这样,s 继承了 SpecialAccount,后者继承了 Account。 当我们执行:

s:deposit(100.00)

Lua 在 s 中找不到 deposit 域,他会到 SpecialAccount 中查找,在 SpecialAccount 中找不到,会到 Account 中查找。使得 SpecialAccount 特殊之处在于,它可以重定义从父类中继承来的方法 :

function SpecialAccount:withdraw(v)
	if v - self.balance >= self:getLimit() then 
		error"insufficient funds"
	end
	self.balance =self.balance-v
end

function SepcialAccount::getLimit()
	return self.limit or 0
end

在,当我们调用方法 s:withdraw(200.00),Lua 不会到 Account 中查找,因为它第一次救在 SpecialAccount 中发现了新的 withdraw 方法,由于 s.limit 等于 1000.00(记住我们创建 s 的时候初始化了这个值)程序执行了取款操作,s 的 balance 变成了负值。

在Lua中面向对象有趣的方面是你不需要创建一个新类去指定一个新的行为。如果仅仅一个对象需要特殊的行为,你可以直接在对象中实现。

2.3多重继承

由于Lua中的对象不是元生(primitive)的,所以在Lua有很多方法可以实现面向对象的程序设计。我们前面所见到的使用index,metamethod的方法可能是简洁、性能、灵活各方面综合最好的。然而,针对一些特殊情况也有更适合的实现方式。下面我们在Lua中多重继承的实现。

实现的关键在于:将函数用作_index。记住,当一个表的metatable存在一个 _index函数时,如果Lua调用一个原始表不存在的函数,Lua将调用这个_index指定的函数。这样可以用_index实现在多个父类查找子类不存在的域。

多重继承意味着一个类拥有多个父类,所以,我们不能用创建一个类的方法去创建子类。取而代之的是,我们定义一个特殊的函数createClass来完成这个功能,将被创建的新类的父类作为这个函数的参数。这个函数创建一个表来表示新类,并且将它的metatable设定为一个可以实现多继承的__index metamethod。尽管是多重继承,每一个实例依然属于一个在其中能找得到它需要的方法的单独的类。所以,这种类和父类之间的关系与传统的类与实例的关系是有区别的。特别是,一个类不能同时是其实例的metatable又是自己的metatable。在下面的实现中,我们将一个类作为它的实例的metatable,创建另一个表作为类的metatable:

-- look up for 'k' in list of tables 'plist'
local function search (k,plist)
	for i=1,#plist do
		local v=plist[i][k]			--try 'i'-th superclass
		if v then return v end
	end
end

function createClass (...)
	local c = {}	--new class
	--class will search for each method in the list of its
	--parents('arg' is the list of parents)
	setmetatable(c,{__index=function(t,k)
	return search(k,arg)
end})

--prepare 'c' to be the metatable of its instances
c.__index = c

--define a new contrutor for this new class
function c:new(o)
	o = o or {}
	setmetatable(o,c)
	return o
end

--return new class
	return c
end

让我们用一个小例子阐明一下 createClass 的使用,假定我们前面的类 Account 和另一个类 Named,Named 只有两个方法 setname and getname:

Named = {}
function Named:getname ()
	return self.name
end
function Named:setname (n)
	self.name = n
end

为了创建一个继承于这两个类的新类,我们调用createClass:

NameAccount = createClass(Account,Named)

为了创建和使用实例,我们像通常一样:

account = NamedAccount:new{name="Paul"}
print(account:getname())			-->Paul

现在我们看看上面最后一句发生了什么,Lua 在 account 中找不到 getname,因此他查找 account 的 metatable 的_index,即 NamedAccount。但是,NamedAccount 也没有getname,因此 Lua 查找 NamedAccount 的 metatable 的__index,因为这个域包含一个函数,Lua 调用这个函数并首先到 Account 中查找 getname,没有找到,然后到 Named 中查找,找到并返回最终的结果。当然,由于搜索的复杂性,多重继承的效率比起单继承要低。一个简单的改善性能的方法是将继承方法拷贝到子类。使用这种技术,index 方法如下 :

...
setmetatable(c,{__index=function(t,k)
	local v=search(k,arg)
	t[k]=v			--save for next access
	return v
end})
...

应用这个技巧,访问继承的方法和访问局部方法一样快(特别是第一次访问)。缺点是系统运行之后,很难改变方法的定义,因为这种改变不能影响继承链的下端 。

2.4私有性(privacy)

Lua中的主要对象设计不提供私有性访问机制。部分原因因为这是我们使用通用数据结构tables来表示对象的结果。

然而,Lua的另一个目标是灵活性,提供程序员元机制(meta-mechanisms),通过他你可以实现很多不同的机制。虽然Lua中基本的面向对象设计并不提供私有性访问的机制,我们可以用不同的方式来实现他。设计的基本思想是:每个对象用两个表来表示:一个描述状态;另一个描述操作(或者叫接口)。对象本身通过第二表来访问,也就是说,通过接口来访问对象。为了避免未授权的范围更,表示状态的表中不涉及操作;表示操作的表也涉及到状态,取而代之的是,状态被保存在方法的闭包内。例如,用这种设计表述我们的银行账号,我们使用下面的函数工厂创建新的对象 :

function newAccount(intiialBalance)
	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,代替的是直接访问self。因为没有这个额外的参数,我们不能使用冒号语法来访问这些对象。函数只能像其他函数一样调用:

accl = newAccount(100.00)
accl.withdraw(40.00)
print(accl.getBalance())			--> 60		

这种设计实现了任何存储在self表中的部分都是私有的,newAccount返回之后,没有什么办法可以直接访问对象,我们只能通过newAccount中定义的函数来访问他。虽然我们的例子中仅仅将一个变量放到私有表中,但是我们可以将对象的任何的部分放到私有表中。我们也可以定义私有方法,他们看起来像公有的,但是我们并不将其放到接口中。例如,我们的账号可以给某些用户取款享有额外的 10%的存款上限,但是我们不想用户直接访问这种计算的详细信息,我们实现如下:

funciton newAccount(initialBalance)
	local self={
		balance=initialBalance,
		LIM=1000.00,
	}
	
	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
	...

这样,对于用户而言就没有办法直接访问 extra 函数了 。

2.5Single-Method的对象实现方法

前面的 OO 程序设计的方法有一种特殊情况:对象只有一个单一的方法。这种情况下,我们不需要创建一个接口表,取而代之的是,我们将这单一的方法作为对象返回。其实,一个保存状态迭代子函数即使一个single-method对象。

关于single-method的选ing一个有趣的情况是:当这个single-method实际是一个基于重要参数而执行不同的任务的分派(dispatch)方法时。针对这种对象:

function newObject(value)
	return function(action,v)
		if action == "get" then return value
		elseif action == "set"	tthen value = v
		else error("invalid action")
		end
	end
end

使用起来很简单:

d = newObject(0)
print(d("get"))		-->0
d("set",10)
print(d("get"))		-->10

这种非传统的对象实现是非常有效的,语法d("set,10")虽然很罕见,但也只不过比传统的d:set(10)长两个字符而已。每一个对象使用一个单独的闭包,代价比起表来小的多。这种方式没有继承担忧私有性:访问对象状态的唯一方法是通过它的内部方法。

posted @ 2020-09-04 13:58  Ligo丶  阅读(202)  评论(0编辑  收藏  举报