Lua程序转载 - 面向对象的实现探讨

>

转载出处:http://blog.csdn.net/xenyinzen/article/details/3536708

元表概念
     Lua中,面向对向是用元表这种机制来实现的。首先,一般来说,一个表和它的元表是不同的个体(不属于同一个表),在创建新的table时,不会自动创建元表。但是,任何表都可以有元表(这种能力是存在的)。

e.g.
t = {}
print(getmetatable(t))   --> nil
t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)

setmetatable( 表1, 表2) 将表2挂接为表1的元表,并且返回经过挂接后的表1。

元表中的__metatable字段,用于隐藏和保护元表。当一个表与一个赋值了__metatable的元表进行挂接时,用getmetatable操作这个表,就会返回__metatable这个字段的值,而不是元表!用setmetatable操作这个表(即给这个表赋予新的元表),那么就会引发一个错误。

table: 0x9197200
Not your business
lua: metatest.lua:12: cannot change a protected metatable
stack traceback:
    [C]: in function ‘setmetatable‘
    metatest.lua:12: in main chunk
    [C]: ?

__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应该说,是一种原型语言。原型是一种常规的对象,当其他对象(类的实例)遇到一个未知的操作时,原型会去查找这个原型。在这种语言中要表示一个类,只需创建一个专用作其他对象的原型。实际上,类和原型都是一种组织对象间共享行为的方式。

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 = {
    x = 10,
    y = 20
}

function A:new( t )
    local t = t or {}
    self.__index = self
    setmetatable( t, self )
    return t
end

从A中产生一个对象AA

AA = A:new()

此时,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的实例对象了。

Lua程序转载 - 面向对象的实现探讨,布布扣,bubuko.com

时间: 2024-11-03 21:57:39

Lua程序转载 - 面向对象的实现探讨的相关文章

lua中的面向对象编程

简单说说Lua中的面向对象 Lua中的table就是一种对象,看以下一段简单的代码: 上述代码会输出tb1 ~= tb2.说明两个具有相同值得对象是两个不同的对象,同时在Lua中table是引用类型的.我在<Lua中的模块与包>中也总结了,我们是基于table来实现的模块,在table中可以定义函数,也就是说,每个table对象都可以拥有其自己的操作.看一段代码: 上面的代码创建了一个新函数,并将该函数存入Account对象的withDraw字段中,然后我们就可以调用该函数了.不过,在函数中使

Lua语言基础汇总(12)-- Lua中的面向对象编程

简单说说Lua中的面向对象 Lua中的table就是一种对象,看以下一段简单的代码: 1 2 3 4 5 6 7 8 9 10 11 12 local tb1 = {a = 1, b = 2} local tb2 = {a = 1, b = 2} local tb3 = tb1   if tb1 == tb2 then      print("tb1 == tb2") else      print("tb1 ~= tb2") end   tb3.a = 3 pri

黑马程序员-面向对象-Object类

---------------------- <a href="http://edu.csdn.net"target="blank">ASP.Net+Unity开发</a>.<a href="http://edu.csdn.net"target="blank">.Net培训</a>.期待与您交流! ---------------------- 一.Object类中的equals(

Cocos2d-x 脚本语言Lua中的面向对象

Cocos2d-x 脚本语言Lua中的面向对象 面向对象不是针对某一门语言,而是一种思想.在面向过程的语言也能够使用面向对象的思想来进行编程. 在Lua中,并没有面向对象的概念存在,没有类的定义和子类的定义.但相同在Lua中能够利用面向对象的思想来实现面向对象的类继承. 一.复制表的方式面向对象 --Lua中的面向对象 --[[ 复制表方式面向对象 參数为一张表.通过遍历这张表取值,赋给一张空表,最后返回新建的表.来达到克隆表 ]] function clone(tab) local ins =

使用 node.js 开发前端打包程序 ---转载

我们在做前端开发的时候经常会在部署上线的时候做程序的打包和合并,我们接下来就会对如何使用 node.js 开发前端打包程序做非常深入的讲解,希望能够帮到有需要的同学. 我们现在做前端开发更多的是多人共同协作开发,每个人负责不同的模块,便于开发和调试.这样就导致我们最后部署上线的时候需要把所有人开发的模块进行合并,生成单个或多个文件上线.如果手动合并的话肯定是费时又费力,而且非常容易出错,所以我们一般都是通过一些工具来实现自动合并的功能. 打包程序的原理非常简单,入口文件->寻找依赖关系->替换

黑马程序猿-面向对象-多态

---------------------- <a href="http://edu.csdn.net"target="blank">ASP.Net+Unity开发</a>.<a href="http://edu.csdn.net"target="blank">.Net培训</a>.期待与您交流! ---------------------- 一多态的前提:(1)要有继承关系(或

黑马程序员-面向对象-多态

---------------------- <a href="http://edu.csdn.net"target="blank">ASP.Net+Unity开发</a>.<a href="http://edu.csdn.net"target="blank">.Net培训</a>.期待与您交流! ---------------------- 一多态的前提:(1)要有继承关系(或

黑马程序员——面向对象编程三大特性

黑马程序员——面向对象编程三大特性 ------- android培训.IOS培训.期待与您交流! ---------- 面向对象编程三大特性:封装,继承,多态,(抽象) ***封装的概念与原理*** 封装是面向对象的特性之一 封装的好处:降低数据被无用的可能性,保证数据安全性 不封装的缺点:会把自己的属性暴露给外部,失去对该属性的管理权 封装原理:实例变量默认只能被当前类的对象方法访问 **封装实现** 定义设置实例变量(setter)方法和访问实例变量(getter)方法 1)setter(

黑马程序员-面向对象的特征

Java面向对象 面向对象是一种程序设计方法,或者是程序设计规范,其基本思想是使用对象.类.继承.封装.多态等基本概念来进行程序设计. 面向对象是一种符合人们思考习惯的思想,可以将复杂的事情简单化,将程序员从执行者转换成了指挥者 完成需求时: ? 先要去找具有所需的功能的对象来用. ? 如果该对象不存在,那么创建一个具有所需功能的对象. ? 这样简化开发并提高代码的复用. 面向对象的开发过程其实就是不断的创建对象,使用对象,指挥对象做事情.设计的过程其实就是在管理和维护对象之间的关系. 面向对象