玩技术,Geeker
一个原创技术文章分享网站

Lua中的面向对象编程

简单说说Lua中的面向对象

Lua中的table就是一种对象,看以下一段简单的代码:

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
print(tb1.a)

上述代码会输出tb1 ~= tb2。说明两个具有相同值得对象是两个不同的对象,同时在Lua中table是引用类型的。我在《Lua中的模块与包》中也总结了,我们是基于table来实现的模块,在table中可以定义函数,也就是说,每个table对象都可以拥有其自己的操作。看一段代码:

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

Account.withDraw(10) -- 调用函数
print(Account.balance)

上面的代码创建了一个新函数,并将该函数存入Account对象的withDraw字段中,然后我们就可以调用该函数了。不过,在函数中使用全局名称Account是一个不好的编程习惯,因为这个函数只能针对特定对象工作,并且,这个特定对象还必须存储在特定的全局变量中。如果改变了对象的名称,withDraw就再也不能工作了。例如以下代码:

a = Account
Account = nil
a.withDraw(100)

这样就会出现错误。我在这里使用Account创建了一个新的对象a,当将Account赋值为nil时,应该要对a对象不产生任何影响。但是,由于在函数withDraw内部使用了Account,而不是变量a,所以就出现了错误。如果我们将withDraw函数内部的Account.balance = Account.balance – v语句修改为:a.balance = a.balance – v,这样就不会出现错误了。这就表明,当我们需要对一个函数进行操作时,需要指定实际的操作对象,即这里的a,这就需要一个额外的参数来表示该操作者,就好比C++中的this一样,只不过这里将这个关键字换成了self,换完以后的代码如下:

Account = {balance = 0}
function Account.withDraw(self, v)
     self.balance = self.balance - v
end

a = Account
Account = nil
a.withDraw(a, 100)
print(a.balance)

这样再调用,就不会出现错误了。

使用self参数是所有面向对象语言的一个核心。大多数面向对象语言都对程序员隐藏了self参数,从而使得程序员不必显示地声明这个参数。Lua也可以,当我们在定义函数时,使用了冒号,则能隐藏该参数,那么上述代码使用冒号来改下,就是下面这个样子了。

Account = {balance = 0}
function Account:withDraw(v) -- 注意这里的冒号":"
     self.balance = self.balance - v
end

a = Account
Account = nil
a:withDraw(100) -- 注意这里的调用时,也需要冒号":"
print(a.balance)

冒号的作用很简单,就是在方法定义中添加一个额外的隐藏参数,以及在一个方法调用中添加一个额外的实参。冒号只是一种语法便利,并没有引入任何新的东西;如果你愿意,你可以可以不使用self,而是在每次定义一个函数时,手动的加上self,只要你处理好了self,它们都是一样的。

这里乱乱的讲了一些Lua中的东西,主要还是说了table是一个不一样的东西,还有self。接下来,就正式进入面向对象的世界。不要忘了,上面总结的东西是非常有用的。

类是什么?一个类就是一个创建对象的模具。例如C++中,每个对象都是某个特定类的实例。在C++中,如果一个类没有进行实例化,那这个类中对应的操作,基本就是一堆“没有用”的代码;而Lua则不一样,即使你不实例化一个“类”,你照样也可以使用“类”名直接调用它的方法(对于C++,请忽视静态的方法);这说明Lua中的“类”的概念与C++这种高级语言中类的概念还是有差别的。在Lua中则没有类的概念,而我们都是通过Lua现有的支持,去模拟类的概念。在Lua中,要表示一个类,只需创建一个专用作其他对象的原型(prototype)。原型也是一种常规的对象,也就是说我们可以直接通过原型去调用对应的方法。当其它对象(类的实例)遇到一个未知操作时,原型会先查找它。

在Lua中实现原型是非常简单的,比如有两个对象a和b,要让b作为a的原型,只需要以下代码就可以完成:

setmetatable(a, {__index = b})  -- 又是元表,不会的请看前几篇关于元表的文章

设置了这段代码以后,a就会在b中查找所有它没有的操作。若将b称为是对象a的“类”,就仅仅是术语上的变化。现在我就从最简单的开始,要创建一个实例对象,必须要有一个原型,就是所谓的“类”,看以下代码:

local Account = {}  -- 一个原型

好了,现在有了原型,那如何使用这个原型创建一个“实例”呢?接着看以下代码:

function Account:new(o)  -- 这里是冒号哦
     o = o or {}  -- 如果用户没有提供table,则创建一个
     setmetatable(o, self)
     self.__index = self
     return o
end

当调用Account:new时,self就相当于Account。接着,我们就可以调用Account:new来创建一个实例了。再看:

local a = Account:new{value = 100} -- 这里使用原型Account创建了一个对象a
a:display()

上面这段代码是如何工作的呢?首先使用Account:new创建了一个新的实例对象,并将Account作为新的实例对象a的元表。再当我们调用a:display函数时,就相当于a.display(a),冒号就只是一个“语法糖”,只是一种方便的写法。我们创建了一个实例对象a,当调用display时,就会查找a中是否有display字段,没有的话,就去搜索它的元表,所以,最终的调用情况如下:

getmetatable(a).__index(display(a))

a的元表是Account,Account的__index也是Account。因此,上面的调用也可以使这样的:

Account.display(a)

所以,其实我们可以看到的是,实例对象a表中并没有display方法,而是继承自Account方法的,但是传入display方法中的self确是a。这样就可以让Account(这个“类”)定义操作。除了方法,a还能从Account继承所有的字段。

继承不仅可以用于方法,还可以作用于字段。因此,一个类不仅可以提供方法,还可以为实例中的字段提供默认值。看以下代码:

local Account = {value = 0}
function Account:new(o)  -- 这里是冒号哦
     o = o or {}  -- 如果用户没有提供table,则创建一个
     setmetatable(o, self)
     self.__index = self
     return o
end

function Account:display()
     self.value = self.value + 100
     print(self.value)
end

local a = Account:new{} -- 这里使用原型Account创建了一个对象a
a:display() --(1)
a:display() --(2)

在Account表中有一个value字段,默认值为0;当我创建了实例对象a时,并没有提供value字段,在display函数中,由于a中没有value字段,就会查找元表Account,最终得到了Account中value的值,等号右边的self.value的值就来源自Account中的value。调用a:display()时,其实就调用以下代码:

a.display(a)

在display的定义中,就会变成这样子:

a.value = getmetatable(a).__index(value) + 100

第一次调用display时,等号左侧的self.value就是a.value,就相当于在a中添加了一个新的字段value;当第二次调用display函数时,由于a中已经有了value字段,所以就不会去Account中寻找value字段了。

继承

由于类也是对象(准确地说是一个原型),它们也可以从其它类(原型)获得(继承)方法。这种行为就是继承,可以很容易的在Lua中实现。现在我们有一个类(原型,其实在Lua中说类这个概念,还是很别扭的,毕竟用C++的脑袋去想,还是觉的有点奇怪的。)CA:

local CA = {value = 0}

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

function CA:display()
     print(self.value)
end

function CA:addValue(v)
     self.value = self.value + v
end

现在需要从这个CA类派生出一个子类CLittleA,则需要创建一个空的类,从基类继承所有的操作:

local CLittleA = CA:new()

现在,我创建了一个CA类的一个实例对象,在Lua中,现在CLittleA既是CA类的一个实例对象,也是一个原型,就是所谓的类,就相当于CLittleA类继承自CA类。再如下面的代码:

local s = CLittleA:new{value1 = 10}

CLittleA从CA继承了new;不过,在执行CLittleA:new时,它的self参数表示为CLittleA,所以s的元表为CLittleA,CLittleA中字段__index的值也是CLittleA。然后,我们就会看到,s继承自CLittleA,而CLittleA又继承自CA。当执行s:display时,Lua在s中找不到display字段,就会查找CLittleA;如果仍然找不到display字段,就查找CA,最终会在CA中找到display字段。可以这样想一下,如果在CLittleA中存在了display字段,那么就不会去CA中再找了。所以,我们就可以在CLittleA中重定义display字段,从而实现特殊版本的display函数。

多重继承

说到多重继承,我在写C++代码的时候也用的很少,一般都是使用组合的方式解决的,对于“组合”这个概念不明白的朋友,可以阅读我的设计模式系列的文章。既然说到了Lua中的多重继承,那也总结一下,顺便开拓一下视野和知识面。

实现单继承时,依靠的是为子类设置metatable,设置其metatable为父类,并将父类的__index设置为其本身的技术实现的。而多继承也是一样的道理,在单继承中,如果子类中没有对应的字段,则只需要在一个父类中寻找这个不存在的字段;而在多重继承中,如果子类没有对应的字段,则需要在多个父类中寻找这个不存在的字段。果冻想 | 一个原创文章分享网站

就像上图表示一样,Lua会在多个父类中逐个的搜索display字段。这样,我们就不能像单继承那样,直接指定__index为某个父类,而是应该指定__index为一个函数,在这个函数中指定搜索不存在的字段的规则。这样便可实现多重继承。这里就出现了两个需要去解决的问题:

  1. 保存所有的父类;
  2. 指定一个搜索函数来完成搜索任务。

对于以上的多重继承,我们来看一段头疼的代码:

-- 在多个父类中查找字段k
local function search(k, pParentList)
    for i = 1, #pParentList do
        local v = pParentList[i][k]
        if v then
            return v
        end
    end
end

function createClass(...)
    local c = {} -- 新类
    local parents = {...}

    -- 类在其元表中搜索方法
    setmetatable(c, {__index = function (t, k) return search(k, parents) end})

    -- 将c作为其实例的元表
    c.__index = c

    -- 为这个新类建立一个新的构造函数
    function c:new(o)
        o = o or {}
        setmetatable(o, self)

        -- self.__index = self 这里不用设置了,在上面已经设置了c.__index = c
        return o
    end

    -- 返回新的类(原型)
    return c
end

-- 一个简单的类CA
local CA = {}
function CA:new(o)
    o = o or {}
    setmetatable(o, {__index = self})
    self.__index = self
    return o
end

function CA:setName(strName)
    self.name = strName
end

-- 一个简单的类CB
local CB = {}
function CB:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

function CB:getName()
    return self.name
end

-- 创建一个c类,它的父类是CA和CB
local c = createClass(CA, CB)

-- 使用c类创建一个实例对象
local objectC = c:new{name = "Jelly"}

-- 设置objectC对象一个新的名字
objectC:setName("JellyThink")
local newName = objectC:getName()
print(newName)

代码虽然头疼,但是还的继续看。首先大体阅读一下上面的代码,看不懂不要紧。现在我来解释上面的代码。

  1. 使用createClass创建了一个类(原型),将CA和CB设置为这个类(原型)的父类(原型);在创建的这个类(原型)中,设置了该类的__index为一个search函数,在这个search函数中寻找在创建的类中没有的字段;
  2. 创建的新类中,有一个构造函数new;这个new和之前的单继承中的new区别不大,很好理解;
  3. 调用new构造函数,创建一个实例对象,该实例对象有一个name字段;
  4. 调用object:setName(“JellyThink”)语句,设置一个新的名字;但是在objectC中没有这个字段,怎么办?好了,去父类找,先去CA找,一下子就找到了,然后就调用了这个setName,setName中的self指向的是objectC;设置以后,就相当于修改了objectC字段的name值;
  5. 调用objectC:getName(),objectC还是没有这个字段。找吧,CA也没有,那就接着找,在CB中找到了,就调用getName,在getName中的self指向的是objectC。所以,在objectC:getName中返回了objectC中name的值,就是“JellyThink”。

还有什么?什么也没有了,对于多重继承,貌似看起来很难,很麻烦,其实也就这么点东西。不懂的话,再来一遍。

我拿什么保护你

我们都知道,在C++或Java中,对于类中的成员函数或变量都有访问权限的。public,protected和private这几个关键字还认识吧。那么在Lua中呢?Lua中是本身就是一门“简单”的脚本语言,本身就不是为了大型项目而生的,所以,它的语言特性中,本身就没有带有这些东西,那如果非要用这样的保护的东西,该怎么办?我们还是“曲线救国”。思想就是通过两个table来表示一个对象。一个table用来保存对象的私有数据;另一个用于对象的操作。对象的实际操作时通过第二个table来实现的。为了避免未授权的访问,保存对象的私有数据的表不保存在其它的table中,而只是保存在方法的closure中。看一段代码:

function newObject(defaultName)
     local self = {name = defaultName}
     local setName = function (v) self.name = v end
     local getName = function () return self.name end
     return {setName = setName, getName = getName}
end

local objectA = newObject("Jelly")
objectA.setName("JellyThink") -- 这里没有使用冒号访问
print(objectA.getName())

这种设计给予存储在self table中所有东西完全的私密性。当调用newObject返回以后,就无法直接访问这个table了。只能通过newObject中创建的函数来访问这个self table;也就相当于self table中保存的都是私有的,外部是无法直接访问的。大家可能也注意到了,我在访问函数时,并没有使用冒号,这个主要是因为,我可以直接访问的self table中的字段,所以是不需要多余的self字段的,也就不用冒号了。

总结

这篇文章对Lua中的“面向对象”进行了一些简单的总结,本来Lua就很简单,我们只是使用了Lua本身的特性去实现一些更高大上的特性,这样没有什么不好,有的时候也没有什么好。要不要用面向对象的这些特性,在具体项目中具体分析,至于如何理解面向对象的概念,不是这里的重点。Lua的面向对象,总结到此为止,以后有了实际的项目应用,接着总结,这篇基础篇的文章,希望对大家有用。

这篇文章我是分4天写完的,时间都很碎,可能有些地方写的不是很连贯,希望大家将就看了。今天七夕,一个人还在写博客,好过分,没办法,一个人就是这样。

2014年8月2日 于深圳。

打赏

未经允许不得转载:果冻想 » Lua中的面向对象编程

分享到:更多 ()

评论 42

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #19

    a = AccountAccount = nila.withDraw(100a =Account 两个对象共同指向同一内存,指向同一个表,Account=nil,Account 就不再指向原先的内存(表),但a还是指向最初始的表的,为什么会出错了?如果没有Account=nil,上述代码还报错吗?

    Hellowei3年前 (2014-09-09)回复
    • 很高兴你能和我讨论你的想法。由于Account == nil.执行以下语句时:Account.balance = Account.balance – vAccount为nil,所以就报错了。如果没有Account=nil这句,就不会出错的;不知道你对C或者C++中的指针是否熟悉,和这里是一个类似的道理。CObj *p = new CObj;CObj *p1 = p;p = NULL;p1->xxxx;(调用CObj的方法)还是会成功的。但是,p->xxxx;就会报错。这个道理是类似的。希望我讲的,你能明白。

      果冻想3年前 (2014-09-10)回复
      • 博主,在c++中,如果这个xxxx是个普通函数的话,而函数中没有引用到成员变量,是不会导致崩溃的,因为p1-L>xxxx只是一个符号地址。

        yuanmu1年前 (2016-01-13)回复
        • 是的,每个函数加参数的确只是一个符号,确定一个函数

          果冻想1年前 (2016-01-14)回复
  2. #18

    谢谢你的耐心回复我也正是因为想到你所举的这个例子才产生疑问的,囧~table表是引用型的,a = table1, b = a;表明a,b同时指向table1这块内存(不知道能不能这样说)。a= nil,后b还是指向table1的,我明白这时不允许a.XXX。但为什么b.XXX也会出错了晚安~

    Hellowei3年前 (2014-09-11)回复
    • 半夜从被窝里爬出来,就只为回答你的问题。以下代码:local tb1 = {1, 2, 3}local tb2 = tb1print(tb1 )tb1 = nil– print(tb1 ) 抛出异常print(tb2 ) — 正常table是引用,Lua中,table,function,thread,userdata都是引用。>>但为什么b.XXX也会出错了你说的这句,我不是很明白。>>晚安~Good night~ 很高兴你能和我继续讨论。

      果冻想3年前 (2014-09-11)回复
  3. #17

    非常感谢~a = AccountAccount = nila.withDraw(100)–不允许local tb1 = {1, 2, 3}local tb2 = tb1tb1 = nilprint(tb2 )—允许第一个为什么不允许访问withDraw字段晚安~

    Hellowei3年前 (2014-09-11)回复
    • 由于调用了以下代码:Account.balance = Account.balance – v上述代码调用出错。Account = nil了,所以,就会出现问题。

      果冻想3年前 (2014-09-12)回复
  4. #16

    学习了,谢谢,写的循循渐进,又不乏深度。大赞。

    刘先生的帐号3年前 (2014-09-24)回复
  5. #15

    看了博主的文章很有收获,可以有一点不太清楚,在数据和方法分开保存在两个表的清空下,如何实现继承呢?请博主指教,谢谢。

    4191700792年前 (2015-02-13)回复
  6. #14

    看了博主的文章很有收获,可以有一点不太清楚,在数据和方法分开保存在两个表的情况下,如何实现继承呢?请博主指教,谢谢。

    4191700792年前 (2015-02-13)回复
  7. #13

    看了博主的文章很有收获,可是有一点不太清楚,在数据和方法分开保存在两个表的情况下,如何实现继承呢?请博主指教,谢谢。

    4191700792年前 (2015-02-13)回复
    • 关于lua中的继承等面向对象的因素,都要使用元表,__index和__newindex等函数。

      果冻想2年前 (2015-02-14)回复
  8. #12

    […] 来源网址:http://www.jellythink.com/archives/529 […]

  9. #11

    […] ??本文链接:http://www.jellythink.com/archives/529 ??订阅本站:http://www.jellythink.com/feed ??转载请注明来源:果冻想???《Lua中的面向对象编程》 if (typeof DUOSHUO !== 'undefined') DUOSHUO.EmbedThread('.ds-thread'); […]

  10. #10

    function Account:new(o) — 这里是冒号哦
    o = o or {} — 如果用户没有提供table,则创建一个
    setmetatable(o, self)
    self.__index = self
    return o
    end
    ==self._inidex = self 这里为什么不是o._index = self呢?

    Starlzd2年前 (2015-07-16)回复
    • > setmetatable(o, self)
      设置o的元表为self;

      > self.__index = self
      表示,如果在o中找不到对应的操作或者变量,就去元表中找__index对应的表或者元方法;
      如果设置成o.__index = self,此时o不是o的元表。

      果冻想2年前 (2015-07-16)回复
      • 不明白,这个self不是Account这个table吗?这个table的_index设置为自己??这个有什么用?

        Starlzd2年前 (2015-07-20)回复
  11. #9

    self.__index = self
    大神,这里不会死循环吗?

    Starlzd2年前 (2015-07-27)回复
    • 没事了,看懂了。。。

      Starlzd2年前 (2015-07-27)回复
  12. #8

    function Account:new(o) — 这里是冒号哦
    o = o or {} — 如果用户没有提供table,则创建一个
    setmetatable(o, self)
    self.__index = self
    return o
    end
    ==self._index = self ,Account 将自己设为自己的元表 有什么用吗?
    a中找不到display函数会到 Account中找(因为setmetatable(o, self)这句 将Account设置为 a的元表),在Account 中找到就执行display就好了。这和有 self.__index = self 有关系吗?有人说 有 self.__index = self 是为了防止 Account 扩展 改变,还是不理解,请博主解答,谢谢

    Jason2年前 (2015-08-19)回复
  13. #7

    提一个小错误,”getmetatable(a).__index(display(a))”
    这里应该为“getmetatable(a).__index.display(a)”或者”getmetatable(a).__index[“display”](a)”。

    CoCosBoy1年前 (2016-05-30)回复
  14. #6

    A = {aa = 1, bb = 2}
    B = {}
    setmetatable(B, {__index = A})
    这时候A.aa和B.aa是什么关系
    为什么我修改了B.aa不影响A.aa?

    Bing9个月前 (08-30)回复
    • 这个操作相当于在B中新增了aa属性,并且设置了B的aa属性的值。

      果冻想9个月前 (09-01)回复
  15. #5

    继承和实现是一样的操作?有什么区别吗??

    Bing9个月前 (08-30)回复
    • 这个主要是面向对象的概念,继承主要说类,实现主要用于描述接口的。再细说,就需要咬文嚼字了。

      果冻想9个月前 (09-01)回复
      • 说错了,应该是继承和实例化。在c++中,继承是说类之间的关系;实例化是类和对象的关系。那在lua中,我看上面的代码,继承和实例化几乎完全一样?

        Bing9个月前 (09-01)回复
        • 在Lua中有个原型的概念,本来没有“实例化”这种东西。

          果冻想9个月前 (09-01)回复
  16. #4

    看着有点头晕,但还是有点收获。

    urain392个月前 (03-23)回复
  17. #3

    大神你好,请问:setmetatable(o, self)
    self.__index = self

    will4周前 (05-02)回复
  18. #2

    大神你好,请问:
    setmetatable(o, self)
    self.__index = self
    这两句,可否用一句代替?
    setmetatable(o,{__index=self})
    因为我理解,设置元表为self似乎有点浪费,当然也能实现目的.
    因为当访问o中某不存在元素时,会访问其元表中的__index,和元表其他元素没有关系.
    所以我的理解是,元表是一些固定的方法,如__add,__index等,其他元素似乎没有存在价值.
    不知理解对不对…希望大神有空提示下.谢谢分享!

    will4周前 (05-02)回复
  19. #1

    懂了, 谢谢~~~

    will3天前回复

在这里玩技术,享受技术带来的疯狂

捐赠名单关于果冻