lua面向对象详解(1)

项目组有个很好的习惯,每个礼拜都开展技术交流活动,要求由其中一个同事作为主讲人,以该主讲人所擅长的技术方面为主题,进行交流。收到这个任务已经月余,但最近实在太忙,只得趁周末准备一下。学习lua已经是很久远的事情了,虽然每天都在运用,而且应该说运用上还是挺溜的,但毕竟运用自如与以简易明了的语言表达出来还是差距挺大,有些东西虽然理解但要表达出来还是挺有难度,再加上时间紧迫,这里就当时抛砖引玉吧,希望大牛们多多指点,有纰漏不对之处,还请指正。

本人认为,要了解lua的面向对象,需要从三个方面下手:table、元表与元方法、面向对象。

一、lua的table

讲到lua的面向对象,则首先就要了解一下lua中的table。

table是什么?

table是lua中的数据结构机制,用table来实现关联数组,可以表示普通数组、符号表、集合、记录、队列及其他数据结构,也可以来表示模块、包、对象。

如:

localt1 = {1,3,5,7,9}  表示5个数字元素的数组
localtt = {'a','b','c','d'}
fork,v in pairs(tt) do
   print(k,v)
end

可以看到打印为

1   a

2   b

3   c

4   d

其中for k,v in pairs(tt) do 表示循环变量tt中的元素,pairs为一种table迭代器,返回元素的键和值。可知,默认情况下(即不指定键的情况下)table的索引是从1开始的(这和c++的数组下标从0开始有很大区别),且会自动递增,对于没有初始化的元素,索引结构都为nil。所以,lua将nil作为界定数组结尾的标志。

localt1 = {1,2,3,4,5,6}

t1[4]= nil

fork,v in ipairs(t1) do

    print(k,v)

end

输出为

1   1

2   2

3   3

虽然存在t1[5] = 5 和 t1[6] = 6 两个元素,但t1[4]为nil,则lua认为这个数组到索引3就结束了,但用户仍可用t1[5]去索引访问这个元素。

ipairs与pairs类似,但ipairs为从下标1开始依次递增索引数字索引的数组的迭代器。pairs则会无序索引table中的所有元素,包括字符索引和函数等。

localt2 = {1,2,abc = 5,4}

fork,v in ipairs(t2) do

   print(k , v)

end

输出为

1   1

2   2

3   4

中间虽然有一个字符串索引“abc”,但是后面的没有指定索引的元素会继续递增数字索引。

table中可以存储值(数字,字符串等)、函数(地址),也可以存储table。

table 既不是值,也不是变量,而是对象。请记住这句非常重要的话!

初步了解了table之后,不继续深入讲解,进入下一个阶段,毕竟这里主要讲的是lua的面向对象。


前面讲了lua的table,讲得比较粗略,不过table将会在不断的运用中逐渐形成对它的认识,不必一开始就完全搞明白它的全部。从这里开始,将讲解lua的元表和元方法。

二、元表与元方法

我们知道c++ 中不能随便将两个对象相加,除非程序用户自己定义+操作符,指定两个对象相加时需要做的操作。lua也是一样,不能将两个table进行算术操作,但是有一种方法可以实现。

元表和元方法就是用来改变lua 中元素的特定行为的。

lua中对元表的操作有如下方法:

setmetatable(t, mt)   设置 t 的元表为mt ,也可以重新设定

getmetatable(t) 获取 t 的元表,如果t 没有元表则返回nil

这里虽然说是可以对lua中元素的元表进行操作,但实际上,lua代码中只可以对table的元表进行设置,如要设置其他类型值的元表,则必须通过C API来实现。

lua中的每个值都有一个元表,table和userdata可以各自有独立的元表,而其他类型的值则共享该类型所属的单一元表。新建的table不会自动创建元表。

任何table都可以作为任何值的元表,而一组相关的table也可以共享一个通用的元表,此元表表示他们的共同行为。一个table甚至可以作为自己的元表,用于描述特有的行为。

下面以一个元素集合为例,说明元表与元方法的应用:


localSet = {}
functionSet.New(obj)
   obj = obj or {}
   local tt = {}
   for k,v in ipairs(obj) do
      tt[v] = true
   end
   return tt
end
 
functionSet.Union(obja , objb)
   local obj = Set.New()
   for k in pairs(obja) do
      obj[k] = true
   end
   for k in pairs(objb) do
      obj[k] = true
   end
   return obj
end
 
function Set.Intersesion(obja , objb)
   local obj = Set.New()
   for k,v in pairs(obja) do
      obj[k] = objb[k]
   end
   return obj
end
 
function Set.Print(obj)
   io.write("{ ")
   for k in pairs(obj) do
      io.write(k.." ")
   end
   print("}")
end

这里用Set表示一个集合的属性tabel,定义了一个New方法,将传进来的一组值表示成一个集合,这里用一个数组来表示,以该元素值为键,值为true表示存在该元素。

obj= obj or {} 是lua中比较常见的一种写法,用于让元素obj有一个不为nil的值,当用户调用函数没有传table进来时,我们获取的obj就为nil值,这是对obj进行索引迭代就会出错。所以obj不为nil时则为本身,否则为一个空table。

Set的Union函数为处理两个集合的并集,分别将两个集合的元素都按规则填充到集合中即可。Intersection函数则处理两个集合的交集,这里用了一点下技巧,先遍历obja集合,每个k都是集合obja的元素,执行操作

obj[k]= objb[k]

则当k为objb中的元素时,objb[k]为true,所以obj[k]= true将元素k加入到结果集。

当k不为objb中的元素时,objb[k]为nil,所以执行obj[k]= nil不会将k加入到结果集。

localt1 = Set.New{1,2,3,4,5,6}

localt2 = Set.New{4,5,6,7,8,9}

Set.Print(t1)

Set.Print(t2)

输出为

{ 1 2 3 4 5 6 }

{ 4 5 6 7 8 9 }

那么,我们要实现用操作符来求两个集合的并集和交集该怎么办呢?

定义一个用作元表的table

localmet = {}

在Set的New函数中增加

setmetatable(tt, met)

将新建的集合table的元表设置为met,并设置元表两个元方法如下

met.__add= Set.Union

met.__mul= Set.Intersesion

这里用算术运算+计算并集,用*计算交集,样就可以了:

localt3 = t1 + t2

Set.Print(t3)

localt4 = t1 * t2

Set.Print(t4)

输出为

{1 2 3 4 5 6 7 8 9 }

{5 6 4 }

可以看到,t3确实是t1和t2的并集,t4确实是t1和t2的交集。

那么,table的元表又是如何运作的呢?

当lua解析器解析t1 + t2时,如果t1有元表,并且元表中有__add字段,则用t1元表的该元方法,而与t2的元表无关,只有当t1没有找到这种元方法是才会查找t2(注意如果t2的元表的__add元方法与t1不同的情形)。当t1和t2中都找不到__add元方法时,lua就会引发一个错误。

通过这个例子,我们想一下元表和元方法是什么?

我们可以把元表看成事本例中Set集合对象抽象出来的一个类对象(注意不是类,元表是table也是对象),这个对象定义了对于以它为元表的对象某些操作的行为(后面还会讲到,元对象提供的不单是方法,还有值)。

lua中只有对象,没有类,对象可以生成对象,再次强调这句话。

当对table进行索引某个方法或值时,lua首先会查找table本身是否有这样的方法或值,如果有则直接使用。否则,会查找该table是否存在元表,并在查找的元表中查找这样的方法或值。

lua中还定义了一些其他的具有通用操作符的元方法:

算术:__add(加法)、__sub(减法)、__mul(乘法)、__div(除法)、__unm(相反数)、__mod(取模)、__pow(乘幂)、__concat(字符串连接)

关系:__eq(等于)、__lt(小于)、__le(小于等于)

其他几个重要的元方法:

__tostring 可以定义将table表示为字符串的元方法

__metatable 调用setmetatable和getmetatable会用到元表的该元方法。如果手动设置元表的__metatable这个元方法的值,则会对table起到保护的作用,外部将不能访问该table的元方法,更不能调用setmetatable设置新元表。

local tt = {}
local mt = {}
mt.__metatable= "not your business"
setmetatable(tt, mt)
print(getmetatable(tt))
setmetatable(tt, {})

输出为

notyour business

lua:metatable2.lua:6: cannot change a protected metatable

__index做为lua中一个非常重要的元方法,它可以是一个函数也可以是一个table。当它为一个函数时,lua会以table和一个不存在不存在的key(存储的key则直接访问)来调用函数。如果为一个table,lua就以相同的方式来重新访问这个table。什么意思呢?来看下面的一个例子:


local window = {}
window.prototype= {x=10,y=20,width=10,height=10}
window.mt= {}
window.mt.__index= function(tbl , key)
   return window.prototype[key]
end
function window.New(obj)
   obj = obj or {}
   setmetatable(obj , window.mt)
   return obj
end
local tb = window.New{x=20,y=30}
print(tb.x)
print(tb.width)

window表将一些属性存储在prototype中,还有一个作为元表的mt。当调用New函数生成对象tb时,设置了tb的元表为mt。当对tb索引x时,首先查找tb本身是否存在x键,发现存在则直接访问。索引width时,tb没有,lua发现它有元表,就跳转到表mt中,要访问mt必须找到__index元方法,发现是一个函数,则将tb和width作为参数传入调用。

那__index为table时又是什么情况呢,我们来做一个实验:

local window = {}
window.prototype= {x=10,y=20,width=20,height=20}
window.mt= {}
function window.New(obj)
   obj = obj or {}
   setmetatable(obj , window.mt)
   return obj
end
localt = window.New{x=20,y=30}
print(t.width)

这里会输出什么呢?结果是nil。对t索引width时虽然会跳转到元表window.mt上,但是lua并不知如何处理元表window.mt(缺少__index元方法)。

当我们在window.mt = {}之后加上一行:

window.mt.__index= window.prototype

输出就是20了。

也就是说,元方法__index是告诉lua对元表应该如果访问或操作。前面的例子将mt的__index元方法指向一个函数,则lua会调用这个函数。这里将mt的__index指向一个table,则lua就会去访问这个table。

当然,这种访问还会引起其他的一下操作,我们将在面向对象的时候将到。




三、lua面向对象

1、lua中的self

首先来看一个例子:

local Account = {balance = 1000}
function Account.Withdraw(money)
   Account.balance = Account.balance - money
end
 
localaa = Account;
aa.Withdraw(100)
print(aa.balance)        --900
print(Account.balance)   --900
 
Account= nil
--aa.Withdraw(100)  --error : attempt to index upvalue 'Account'(a nil value)

  

当定义aa时,aa实际上是Account的一个“引用”,并不会实际的给aa分配该table的一个拷贝,类似于linux的硬链接或windows的内核对象,执行了Account= nil 之后,实际上这个table还在,只是Account对这个table的链接断裂,我们可以通过输出的两个值可以看出。

把Account 置为 nil 之后再调用Withdraw 函数,由于函数内使用了Account对象,所以会引起访问出错。

在看一个改进版本:

local Account = {balance = 1000}
function Account.Withdraw(self,money)
   self.balance = self.balance - money
end
 
local aa = Account
Account= nil
aa.Withdraw(aa, 100)
print(aa.balance)
aa:Withdraw(100)
print(aa.balance)

在这个版本中,调用Withdraw函数时,使用的是传入函数的self的balance值,这个self现在就是aa的“引用”了。Lua中提供了一个语法糖,使用冒号调用函数时,会把调用对象作为第一个参数传入函数。

请注意函数书写的三种方式:

(1)    table中写函数,需要指定self参数:
local Account =
{
       Withdraw = function(self , money)
       end,
}
(2)    函数外部写函数,不指定self参数:
local Account = {}
function Account:Withdraw(money)
end
(3)函数外部写函数,指定self参数
local Account = {}
function Account.Withdraw(self , money)
end

虽然这里访问可以了,但是,这并不是面向对象,因为对aa中balance的修改实际上就是对Account中的balance修改。这里主要介绍self的用法,self其实与c++中的this指针有相同的作用。

2、继承

Lua中没有类的概念,对象是没有类型的,但是我们可以为一些对象构建相同的共有的行为,这就是原型,不过我们要模拟类也是可以的。由于原型与类在这里的作用相同,这里仍采用类的叫法。

要在lua中实现原型,可以用继承的方法,这里用到了setmetatable方法。

setmetatable(a, {__index = b})

这样就能让b作为a的原型,当要访问a中的某个字段或函数不存在时,就会在b中查找。

我们再来看一下另一个版本:

local AccountClass =
{
   balance = 1000,
   New = function(self , obj)
      obj = obj or {}
      setmetatable(obj , self)
      self.__index = self
      return obj
   end,
   Withdraw = function(self , money)
      self.balance = self.balance - money
   end,
}
 
local aa = AccountClass:New()
local bb = AccountClass:New()
aa:Withdraw(100)
print(aa.balance)     --900
print(bb.balance)     --1000
print(AccountClass.balance)    --1000


这里多了一个New函数,用于生成以AccountClass为原型的对象。当调用

aa:Withdraw(100)

时,会发生什么?首先lua会在aa中查找Withdraw函数,可以看到New函数中生成的aa只不过是一个空的table,并没有这个函数。于是lua就会查找aa是否具有元表,如果有,就继续在元表中查找该方法:

getmetatable(aa).__index.Withdraw(aa,100)

于是就有

AccountClass.Withdraw(aa,100)

这样就调用了AccountClass的方法,但传入的self对象是aa自己,而不是AccountClass。

注意,这里还有一个很有趣的东西:我们知道aa并没有balance字段,那么如果使用aa.balance会不会修改AccountClass对象的balance字段的值呢?我们把过程分解一下:

aa.balance= aa.balance – money

调用时,会先计算等号右边,lua没有在aa中找到balance字段,就会在其元表中查找,转换为:

aa.balance= getmetatable(aa).__index.balance - money

这其实是一个赋值的过程,这个时候会在aa中生成一个balance字段,值为等号右边的值,这样,aa就继承了AccountClass中的值和方法。

通过上面的实验也可以看出,由AccountClass对象New出来的对象都有自己的字段值和相同的方法,我们称这些对象继承自AccountClass。

3、多态

我们先看下面的例子:

local Account = {
   balance = 1000,
   New = function(self , obj)
      obj = obj or {}
      setmetatable(obj , self)
      self.__index = self
      return obj
   end,
   Withdraw = function(self , money)
      self.balance = self.balance - money
   end,
   Deposit = function(self , money)
      self.balance = self.balance + money
   end,
}
 
local SpecialAccount = Account:New()
function SpecialAccount:Withdraw(money)
   self.balance = self.balance - money - 5
end
 
local aa = SpecialAccount:New()
aa:Withdraw(100)
print(aa.balance)     --895
aa:Deposit(100)
print(aa.balance)     --995

这里,Account定义了两个方法,Withdraw和Deposit,我们用这个对象生成对象SpecialAccount。在lua中对象可以生成对象。SpecialAccount定义了自己的Withdraw方法,用SpecialAccount生成对象aa。当对aa进行操作时,lua首先查找aa是否有需要的内容,没有则会到SpecialAccount中查找(注意,生成aa时setmetatable传入的是SpecialAccount,而不是Account),如果没有再到SpecialAccount的元表Account中查找。

基于lua的这种查找顺序,如果调用Withdraw方法在SpecialAccount中查找到了,就不会再到Account去查找。所以,SpecialAccount定义了自己的Withdraw方法,则其生成的对象就会用这个方法,

由于lua中没有类型,不存在向c++中那样用基类的引用或指针来指向派生类,所以这里只能说由Account生成的使用对象会调用Account的Withdraw方法,而SpecialAccount生成的使用对象会调用SpecialAccount中的Withdraw方法。这里更像是函数的覆盖?

(3)多重继承
Lua中实现多重继承与一般的继承不同,因为有多个基类,一种实现如下:
local Account = {
--基类Account
balance = 0,
Withdraw = function(self , v)
self.balance = self.balance - v
end,
Deposit = function(self , v)
self.balance = self.balance + v
end,
}

function Account:New(obj)
obj = obj or {}
setmetatable(obj , Account)
Account.__index = Account
return obj
end

local Name = {
--基类Name
GetName = function(self)
return self.name
end,
SetName = function(self , na)
self.name = na
end,
}

function Name:New(obj)
obj = obj or {}
setmetatable(obj , Name)
Name.__index = Name
return obj
end

function Search(key , pList)
--用于在基类中搜索某个字段
for k in pairs(pList) do
if pList[k][key] then
return pList[k][key]
end
end
return nil
end
function CreateClass(...)
--产生多重派生类
local c = {}
local parent = {...}
setmetatable(c , {__index = function(t,k)--在多重派生类中索引某个字段时到其基类中搜索该字段
return Search(k , parent)
end
})
c.__index = c
function c:New(o)
o = o or {}
setmetatable(o , c)
return o
end
return c
end

local NamedAccount = CreateClass(Account , Name)
local na = NamedAccount:New{name = "Smith"}
print(na:GetName())
NamedAccount为由基类Account和Name派生出来的类,使用CreateClass产生。当调用na:GetName()函数时,首先到na中查找该方法,之后到NamedAccount中查找,再到__index字段查找,发现为函数,则调用该函数,到Account中查找后到Name中查找。由于多重继承在调用基类方法时需要这样查找,性能便下降许多。为了改进性能,可以稍微修改一下:
function CreateClass(...)
--产生多重派生类
local c = {}
local parent = {...}
setmetatable(c , {__index = function(t,k)--在多重派生类中索引某个字段时到其基类中搜索该字段
local v = Search(k , parent)
c[k] = v
return v
end
})
c.__index = c
function c:New(o)
o = o or {}
setmetatable(o , c)
return o
end
return c
end
这样,只有该函数调用一次,就会复制到对象中,下次调用便可以直接使用该对象的方法了。但这种改进也只会影响到该派生类对象,不会继续向下沿用。

(4)私密性(封装)
Lua中由于使用table来表示类,因此没有提供任何私密性的机制,但是可以进行模拟,基本思想是用两个表来表示一个类:一个数据表,用于存储类的数据,另一个为方法类,封装类的方法,用户只能使用方法类。如下:
function NewAccount(initbalance)
local self = {balance = initbalance or 0}
function Deposit(v)
self.balance = self.balance + v
end
function Withdraw(v)
self.balance = self.balance - v
end
function GetBalance()
return self.balance
end
return {
Deposit = Deposit,
Withdraw = Withdraw,
GetBalance = GetBalance
}
end

local a1 = NewAccount()
a1.Withdraw(100)
print(a1.GetBalance())

该实现中,定义了一个self表,用于存放数据,也定义了方法,但实际返回的表只是对方法的封装,将用户使用的方法名与实际的方法名对应起来,这些方法都可以直接访问self数据表,因此不需要额外的self参数。当NewAccount返回后,self中的数据就只能通过返回表的几个方法访问,而不能直接访问,这样就实现了数据的私密性。我们甚至可以封装一些私密的方法,只要不将该方法对应到返回的表中即可:

function NewAccount(initbalance)
local self = {balance = initbalance or 0 , LMT = 10000.0}
function Deposit(v)
self.balance = self.balance + v
end
function Withdraw(v)
self.balance = self.balance - v
end
function GetBalance()
return self.balance + Extra()
end
function Extra()
if self.balance >= self.LMT then
return self.balance * 0.1
else
return 0
end
end
return {
Deposit = Deposit,
Withdraw = Withdraw,
GetBalance = GetBalance
}
end

local a1 = NewAccount(100000)
print(a1.GetBalance())
上例中定义了私有方法Extra,但没有对应到返回的表中,因此它是一个私有的方法。




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值