Lua中,面向对向是用元表这种机制来实现的。元表是个很“道家”的机制,很深遂,很强大,里面有一些基本概念比较难理解透彻。不过,只有完全理解了元表,才能对Lua的面向对象使用自如,才能在写Lua代码的高级语法时游刃有余。
lua的Metatable的官方解释,
1. 每个table和每个full user data都可以有自己的Metatable,并可通过setMetatable和getMetatable进行访问
2. 其他任何lua类型,每种类型共享一个Metatable;比如number类型共享一个Metatable;string类型共享一个Metatable;这些类型的Metatable无法更改,除非用C API
3. Metatable的key叫事件(event),value叫元方法(Metamethod)
4. 除了__index,__newindex可以是function或table外,其他元方法都必须是function
举个例子说明,对于一个非数值类型的值,我们可以修改元表里的"__add"字段里的函数,来定义其加法的行为。这样,对该类型值进行加法运算,就会自动调用__add对应的元方法。如果调用该类型一个未定义的key或者未注册的函数,就会查找__index事件。等等。
首先,一般来说,一个表和它的元表是不同的个体(不属于同一个表),在创建新的table时,不会自动创建元表。
但是,任何表都可以有元表(这种能力是存在的)。
e.g.
t = {}
print(getMetatable(t)) --> nil,现在表t还没有元表
t0 = {}
setMetatable(t,t0) --> 挂接一个元表
assert(getMetatable(t) == t0) --> 表t有元表了,为t0
t1 = {}
setMetatable(t,t1) --> 换一个元表,ok
assert(getMetatable(t) == t1) --> 表t的元表现在为t1了
setMetatable(t,nil) --> 去掉表t的元表,ok
print(getMetatable(t)) --> nil,现在表t没有元表了
setMetatable(表1,表2) 将表2挂接为表1的元表,并且返回经过挂接后的表1。
元表中的__Metatable字段用于隐藏和保护元表。当一个表与一个赋值了__Metatable的元表进行挂接时,用getMetatable *** 作这个表就会返回__Metatable这个字段的值,而不是元表!用setMetatable *** 作这个表(即给这个表赋予新的元表),那么就会引发一个错误。
e.g.
t2 = {}
t3 = { __Metatable = t2 }
t4 = {}
setMetatable(t4,t3)
assert(getMetatable(t4) == t2) --> 表t4的元表不为t3,而是t3中__Metatable的值
t5 = {}
setMetatable(t4,t5) --> 修改t4的元表,报错,因为t4的元表中有__Metatable字段
报错如下:
lua: test.lua:23: cannot change a protected Metatable
stack traceback:
[C]: in function 'setMetatable'
obj.lua:23: in main chunk
[C]: ?
>Exit code: 1
__index方法
元表中的__index元方法,是一个非常强力的元方法,它为回溯查询(读取)提供了支持。而面向对象的实现基于回溯查找。
当访问一个table中不存在的字段时,得到的结果为nil,这是对的,但并非完全正确。实际上,如果这个表有元表的话,这种访问会促使Lua去查找元表中的__index元方法。如果没有这个元方法,那么访问结果就为nil。否则,就由这个元方法来提供最终的结果。
__index可以被赋值为一个函数,也可以是一个表。是函数的时候,就调用这个函数,传入参数(参数是什么后面再说),并返回若干值。是表的时候,就以相同的方式来重新访问这个表。(是表的时候,__index就相当于元字段了,概念上还是分清楚比较好,虽然在Lua里面一切都是值)
注意,这个时候,出现了三个表的个体了。这块很容易犯晕,我们来理一下。
我们直接 *** 作的表,称为表A,表A的元表,称为表B,表B的__index字段被赋予的表,称为表C。整个过程是这样的,查找A中的一个字段,如果找不到的话,会去查看A有没有元表B,如果有的话,就查找B中的__index字段是否有赋值,这个赋值是不是某个表C,如果是的话,就再去C中查找有没有想访问的那个字段,如果找到了,就返回那个字段值,如果没找到,就返回nil。
对于没有元表的表,访问一个不存在的字段,就直接返回一个nil了。
__newindex是对应__index的方法,它的功能是“更新(写)”,两者是互补的。这里不细讲__newindex,但是过程很相似,灵活使用两个元方法会产生很多强大的效果。
从继承特性角度来讲,初步的效果使用__index就可以实现了。
面向对象的实现
一些面向对象的语言中提供了类的概念,作为创建对象的模板。在这些语言里,对象是类的实例。Lua不存在类的概念,每个对象定义他自己的行为并拥有自己的形状(shape)。然而,依据基于原型(prototype)的语言比如Self和NewtonScript,在Lua中仿效类的概念并不难。在这些语言中,对象没有类。相反,每个对象都有一个prototype(原型),当调用不属于对象的某些 *** 作时,会最先会到prototype中查找这些 *** 作。在这类语言中实现类(class)的机制,我们创建一个对象,作为其它对象的原型即可(原型对象为类,其它对象为类的instance)。类与prototype的工作机制相同,都是定义了特定对象的行为。
Lua中实现原型很简单,在上面分析的的那个三个表中,C就是A的原型。
原理讲通后,来一点小技巧。其实,上面说的三个表嘛,不一定就是完全不同的。A和C可以是同一个。看下面的例子。
A = {}
setMetatable( A,{ __index = A } )
这时,相当于A是A自身的原型了,自己是自己的原型,是个很有趣的字眼。就是说在查找的时候,在自己身上找不到就不会去其他地方找了。不过,自身是自身的原型本身并没有多大用的。如果A能做为一个类,然后生成的新对象以A做为原型,这才有用,后面谈。
再看,自身也可以是自身的元表的。即A可以是A的元表。
A = {}
setMetatable( A,A )
这时就可以这样写了,
A.__index = 表或函数
自己是自己的元表有用处的,如果A.__index是赋予的一个表,至少能在内存中少产生一个表;而如果A.__index是一个函数,那么就会产生很简洁强大的效果。(__index为其本身的一个字段了,不是很简洁吗)
然后,元表B与原型表C也可以是同一个。
A = {}
B = {}
B.__index = B
setMetatable( A,B )
这时,一个表的元表,就是这个表的原型,在面向对象的概念里,就是这个表的类。
我们甚至可以这样来写:
A = {}
setMetatable( A,A )
A.__index = A
从语法原理上,是行得通的。但Lua解释器为了避免出现不必要的麻烦(循环定义),把这种情况给Kick掉了,如果这样写,会报错,并提示
loop in gettable
说真的,这样定义也确实没什么用处。
下面开始正式进入面向对象的实现。先引用一下Sputnik中的实现片断,
local Sputnik = {}
local Sputnik_mt = {__Metatable = {},__index = Sputnik}
function new(config,logger)
-- 这里生成obj对象之后,obj的原型就是Sputnik了,而后面会有很多的Sputnik的方法定义
local obj = setMetatable({},Sputnik_mt)
-- 这里的方法就是“继承”的Sputnik的方法
obj:init(config)
返回这个对象的引用
return obj
end
由上面可见,两个表定义加上一个方法,实现了类以及由类产生对象的方案。因为这是在模块中,故new前面没有表名称。这种方式实现有个好处,就是在外界调用此模块的时候,使用
sputnik = require "sputnik"
然后,调用
s = sputnik.new()
就可以生成一个sputnik对象s了,这个对象会继承原型Sputnik(就是上面定义的那个表)的所有方法和属性。
但是,这种方法定义的,也有点问题,就是,类的继承实现上不方便。它只是在类的定义上,和生成对象的方式上比较方便,但是在类之间的继承上不方便。
下面,用另一种方式实现。
A = {balance = 0}
function A:new (o) -- 其实函数的第一个参数是self,表示对象自身,此处lua支持使用冒号 *** 作符省略了第一个参数self,写起来更简洁。用点号 *** 作符的写法为:function A.new(self,t ) end】
o = o or {} -- create object if user does not provIDe one
setMetatable(o,self)
self.__index = self
return o
end
【补充一下,lua中的逻辑运算符:
逻辑运算
and,or,not
其中,and 和 or 与C语言区别特别大。
在这里,请先记住,在Lua中,只有false和nil才计算为false,其它任何数据都计算为true,0也是true!
and 和 or的运算结果不是true和false,而是和它的两个 *** 作数相关。
a and b:如果a为false,则返回a;否则返回b
a or b:如果 a 为true,则返回a;否则返回b
举几个例子:
print(4 and 5) --输出 5
print(nil and 13) --输出 nil
print(false and 13) --输出 false
print(4 or 5) --输出 4
print(false or 5) --输出 5
在Lua中这是很有用的特性,也是比较令人混洧的特性。
我们可以模拟C语言中的语句:x = a? b : c,在Lua中,可以写成:x = a and b or c。
最有用的语句是: x = x or v,它相当于:if not x then x = v end 。
局部变量声明:Lua中的变量作用域控制方式,用local关键字来表示局部变量,其他变量均为全局变量。和C/C++相同,同样作用的情况下,局部变量将覆盖全局变量,即全局变量将“不可见”。
局部变量仅在声明它的该程序块中有效。
在Lua中,有一种习惯写法:
local foo = foo
这句代码创建了一个局部变量foo,用其来保存全局变量foo的值。如果后续其他函数需要改变全局foo的值,那么可以在这里先将它的值保存起来。这种方式还可以加速在当前作用域中对全局变量foo的访问。此时需要访问全局变量foo的话,可以使用_G.foo
lua中使用局部变量有两个好处:
1,避免命名冲突
2,访问局部变量的速度比全局变量快
控制语句:
条件控制只有if-then-else,没有switch-case语句,此外elseif也是一个关键词。
循环语句:
(1)repeat-until:
C++中没有的直到型循环,灰常有新鲜感。。。另外,在repeat-until中,循环内声明的局部变量在条件测试中仍然有效。(仿佛回到了无法无天的VC6.0时代)
(2)数字型for:
for var = from,to,step do
anything
end
其中from或to都可能为函数返回值,但和C++中不同的是,该函数仅在进入循环时执行一次。
(3)泛型for:通过迭代器函数来遍历所有值。
实际上就是一个Lua内建的机制,用来保存迭代器工厂的多个返回值,详见Iterator部分。
】
从A中产生一个对象AA
AA = A:new() -- 调用的时候使用同样使用冒号 *** 作符省略了第一个参数self,也可以使用点号 *** 作符:AA = A.new(A)。冒号的效果相当于在函数定义和函数调用的时候,增加一个额外的隐藏参数
此时,AA就是一个新表了,它是一个对象,但也是一个类。它还可以继续如下 *** 作:
s = AA:new()
AA中本来是没有new这个方法的,但它被赋予了一个元表(同时也是原型),这个时候是A,A中有new方法和x,y两个字段。
AA通过__index回溯到A找到了new方法,并且执行new的代码,同时还会传入self参数。这就是奇妙所在,此时候传入的self参数引用的是AA这个表,而不再是第一次调用时A这个表了。因此 AA:new() 执行后,同样,是生成了一个新的对象s,同时这个对象以AA为原型,并且继承AA的所有内容。至此,我们不是已经实现了类的继承了吗?AA现在是A的子类,s是AA的一个对象实例。后面还可以以此类推,建立长长的继承链。
由上也可见,类与原型概念上还是有区别的,Lua是一种原型语言,这点体现的得很明显,类在这种语言中,就是原型,而原型仅仅是一个常规对象。
下面,如果在A中定义了函数:
function A:acc( v )
self.x = self.x + v
end
function A:dec( v )
if v > self.x then error "not more than zero" end
self.x = self.x - v
end
然后,现在调用
s:acc(5)
那么,是这样调用的,先是查找s中有无acc这个方法,没有找到,然后去找AA中有无acc这个方法,还是没找到,就去A中找有无此方法,找到了。找到后,将指向s的self参数和5这个参数传进acc函数中,并执行acc的代码,执行里面代码的时候,这一句:
self.x = self.x + v
在表达式右端,self.x是一个空值,因为self现在指向的是s,因此,根据__index往回回溯,一直找到A中有一个x,然后引用这个x值,10,因此,上面表达式就变成
self.x = 10 + 5
右边计算得15,赋值给左边,但这时self.x没有定义,但是s(及s的元表)中也没有定义__newindex元方法,于是,就在self(此时为s)所指向的表里面新建一个x字段,然后将15赋值给这个字段。
经过这个 *** 作之后,实例s中,就有一个字段(成员变量)x了,它的值为15。
下次,如果再调用
s:dec(10)
的话,就会做类似的回溯 *** 作,不过这次只做方法的回溯,而不做成员变量x的回溯,因为此时s中已经有x这个成员变量了,执行了这个函数后,s.x会等于5。
综上,这就是整个类继承,及对象实例方法引用的过程了。不过,话还没说完。
AA作为A的子类,本身是可以有一些作为的,因为AA之下的类及对象在查找时,都会先通过它这一关,才会到它的父亲A那里去,因此,它这里可以重载A的方法,比如,它可以定义如下函数:
function AA:acc(v)
...
end
function AA:dec(v)
...
end
函数里面可以写入一些新的不一样的内容,以应对现实世界中复杂的差异性。这个特性用面向对象的话来说,就是子类可以覆盖父类的方法及成员变量(字段),也就是重载。这个特性是必须的。
AA中还可以定义一些A中没有的方法和字段, *** 作是一样的,这里提一下。
Lua中的对象还有一个很灵活强大的特性,就是无须为指定一种新行为而创建一个新类。如果只有一个对象需要某种特殊的行为,那么可以直接在该对象中实现这个行为。也就是说,在对象被创建后,对象的方法和字段还可以被增加,重载,以应对实际多变的情况。而毋须去劳驾类定义的修改。这也是类是普通对象的好处。更加灵活。
可以看出,A:new()这个函数是一个很关键的函数,在类的继承中起了关键性因素。不过为了适应在模块中使用的情况(很多),在function A:new(t)之外还定义一个
function new(t)
A:new(t)
end
将生成函数封装起来,然后,只需使用 模块名.new() 就可以在模块外面生成一个A的实例对象了。
差不多了吧,可以看到,这种类实现的机制是多么自洽,简洁,灵活,强大!不过要折磨下你的大脑了。
综合代码:
-- 对象A,也是一个类型(对象与类是一个东西,称之为原型)
A =
{
x = 10,
y = 20
}
function A:new(o)
o = o or {} -- create object if user does not provIDe one
setMetatable(o,self)
self.__index = self
return o
end
function new(o)
A:new(o)
end
function A:acc(v)
self.x = self.x + v
end
function A:dec(v)
if v > self.x then
error "not more than zero"
end
self.x = self.x - v
end
--从A中产生一个对象AA,也是子类型
AA = A:new()
-- 重载
function AA:acc(v)
self.x = self.x + v
print(self.x)
end
-- 子类中新增的方法
function AA:getx()
return self.x
end
--此时,AA就是一个新表了,它是一个对象,但也是一个类,是AA的子类。它还可以继续如下 *** 作:
s = AA:new()
--然后,现在调用
s:acc(5)
s:dec(10)
print(s:getx())
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////@H_502_337@
-- 综合示例二@H_502_337@
Class = { x = 0 }
function Class:new(oo)
local o = oo or {}
setMetatable(o,self)
self.__index = self
return o
end
function Class:func1(a)
self.x = 100 -- self是通过点号调用成员变量
return self.x
end
function Class:func2()
local a = self:func1() -- self是通过冒号调用成员函数
print(a)
end
-- 创建一个对象
obj1 = Class:new()
obj1:func2() -- 对象通过冒号调用成员函数
-- 通过类名调用成员函数
Class:func2()
-- 定义一个子类
ChildClass = Class:new()
-- 重写父类中的函数
function ChildClass:func1(a)
return 200
end
-- 创建一个对象
obj2 = ChildClass:new()
obj2:func2()
-- 通过类名调用成员函数
ChildClass:func2()
@H_502_337@ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
遗留问题:
全局变量存放在_G表中,局部变量存放在哪里?
函数的参数是局部变量吗?函数体中定义的变量是局部变量吗?@H_502_337@
return 一个局部变量(例如表),是怎么做的?@H_502_337@
@H_502_337@
function f(abc) xzy = xzy or {} pq = 1 return abc end --print(abc) -- nil --print(xyz) -- nil --print(pq) -- nil for k,v in pairs(_G) do print(k) print(v) end @H_502_337@ 总结
以上是内存溢出为你收集整理的Lua中的面向对象实现探讨全部内容,希望文章能够帮你解决Lua中的面向对象实现探讨所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)