欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Lua 模拟面向对象

程序员文章站 2024-03-17 22:00:16
...

分析

Lua中并不存在类的概念,但是我们可以通过使用table和元表元方法来模拟类和类的实例的特性和行为

基础:

Lua的table可以存放各个类型的值,也可以存放函数(函数在Lua中是第一类值与其他值一样使用,可以被存放在变量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值),那么,我们就可以使用table来生成类的原型,存放类的成员值和成员变量。
Lua的元表提供了类似C++ 中继承的特性,如果我将a的元表设置为b,那么a将会拥有b中的值和函数,可以把a看成b的子类。

方案:

用一个table实现类的原型,可以把这个table看成一个类,另一个表使用metatable指定原型table为其元表,那么它就能使用原型的所有方法,再让表的__index元方法指向原型,该表就能使用原型所有的值。

关键:

模拟面向对象可以分解为类的定义和类的实例化两个问题,类的定义主要是实现类的成员值和成员函数的定义,以及如何让子类拥有父类的方法集。类的实例化主要解决实例要如何共享方法集,但独享自己的成员值。

开始模拟

如何定义类成员值和类成员函数

我们将table堪称一个类,table中的值(非函数)看作成员值,table中的函数值看作成员函数
示例

-- 定义类
Account = {}
-- 定义类成员
Account.balance = 0
-- 定义类成员函数
function Account.withDraw(v)
    Account.balance = Account.balance - v
end

Account.withDraw(10)
print(Account.balance) --输出-10

C++中使用new方法创建一个新的对象,并为这个新对象开辟空间。Lua中既没有没有类的概念,也没有new方法,且Lua对table的赋值操作是简单的引用赋值(也就是说只是起了一个别名),但是在面向对象的思想中,类的各个实例之间应该互相不影响,每个对象都是类的一个单独的实例,我们如何让各个实例独享自己的成员值呢?

==错误==:如果直接使用赋值符号,我们的确可以使用成员值和成员函数,但是我们实际上还是在使用原型类本身

-- 定义类
Account = {}
-- 定义类成员
Account.balance = 0
-- 定义类成员函数
function Account.withDraw(v)
    Account.balance = Account.balance - v
end

Account.withDraw(10)
print(Account.balance) --输出-10

a = Account     --赋值引用,a只是Account的一个别名
-- Account = nil --一旦给Account赋值为nil会影响到a,因为a是Accout的引用,这不符合面向对象的思想,面向对象中,不同的对象应该互相不影响
a.withDraw(100)
print(a.balance)

==问题分析==:
前面的这种实现方式有两个问题:
一是,赋值引用的实际上是用一个实例,而我们是要根据原型类创建新的实例,并为实例分配空间
二是:我们要相办法让新定义的table使用元表的方法集操作自己独享的成员值
==解决方案==:
我们自己定义一个类似构造函数的new函数,在new函数里面建立一个新的table,使用Lua为函数提供的额外参数self,当我们需要对一个函数进行操作时,使用self参数指定实际的操作对象,这个额外的参数就像是C++中用来表示类自己的关键字this,Lua中使用关键字self。为了保证实例独享自己的成员值,我们在需要使用到成员值的地方,也就是成员函数定义的时候,直接为成员值定义变量,Lua会自动变量分配地址空间。
解决了以上两个问题,也就实现了原型类的实例化

类的实例化

实例化的思路非常简单,为table构建一个类似C++ 类中构造函数一样的方法,每次调用这个方法都创建一个新的table并把并把self的__index元方法值设为self本身,并在定义原型的成员函数时,使用self参数来指定实际操作的对象,并为操作对象的变量开辟空间(使用self.变量名进行赋值等操作时Lua会自动为该变量分配空间)

为什么要将self__index元方法值设为self本身?
调用new方法的时候,self实际上是父类,将父类的元表设置为父类自己,我们知道一个table(这里指子类对象)的__index元方法默认为nil,因此遇到未定义的变量值(如果父类的某变量没有在函数中被使用,按照我们的实现方案我们不会为其定义对应的变量值,也就是说那些父类中存在但未在函数中使用的变量也会是子类未定义的变量),子类在查找__index字段失败就会去查找元表的__index字段,发现它指向父类本身,于是就去父类中查找该未定义的字段值。如果一个变量值没有在函数中使用个,那么也就是说该变量不会在函数中被改变,我们可以让子类直接访问父类对应的值,我们也不必在为它专门开辟一个变量空间,这种方案时可行的。

-- 改进上述代码
-- 定义类
Account = {}
-- 定义类的构造函数
function  Account:new( o )
    o = o or {} --如果参数提供table,则创建一个table
    setmetatable(o,self) --设置子类实例的元表为原型类
    self.__index = self  --将原型类的__index元方法设置为它本身
    return o
end
-- 定义类的成员值
Account.balance = 0
-- 定义类的带self参数的成员函数
function Account.withDraw(self,v)
    -- 在函数中为实例定义成员值字段,Lua为自动为成员值分配空间
    self.balance = self.balance - v 
    return self.balance
end

--a b是新的table,他们的元表为Account
local a = Account:new()  
local b = Account:new()
print(a)    --table: 00A891C0
print(b)    --table: 00A893A0
-- 打印出来的a b是两个不同的table地址,是两个"类"的实例\

-- 子类实例调用父类方法操作子类独享成员值
print(a.withDraw(a,100)) -- -100
                         -- 调用父类函数withDraw的过程
                         -- a.balance = a.balance - 100 --为a这个子类实例定义了独享变量balance默认值为0,并减去100
                         -- 函数范围值为a.balance计算后的值为-100
print(b.withDraw(b,120)) -- -120 同理
print(Account.balance)   -- 0
--三个实例的balance值各不相同,由此可知他们都有自己独享的成员值

Lua中允许使用冒号来简化书写,冒号就是用于隐藏self参数的一个语法糖,a:withDraw(100)就相当于a.withDraw(a,100)

类的继承

在C++ 中,每个对象都是某个特定类的实例,我们必须使用new方法实例化对象,实例化的对象C++ 才会给分配空间,我们才能在对象的数据上做操作,而在Lua中,不需要程序员自己分配空间,即使不实例化一个“类”,仍旧可以使用“类”名调用它的方法,Lua模拟的类和C++ 的类是有区别的,Lua中本来没有类的概念
Lua中表示一个类,是通过创建一个用于其他对象的原型table,原型也是一个常规对象,我们可以直接通过原型调用对应的方法,当其他对象(“类”的实例)遇到一个未知操作时,会去原型中查找对应的方法,原型类似C++ 继承思想中父类的概念。
在Lua中实现原型非常简单,使用setmetatable指定表的原型

local Account = {}
Account.num = 10
function Account:new(o)
    o = o or {}
    setmetatable(o,self)    --指定新的实例的元表为Account原型类
    self.__index = self
    return o
end
function Account:display()  --为原型类增加display方法
    print(self.num)         --self是隐藏的参数,Account:display()相当于Account.display(self)
end

local aa = Account:new({num=22})    --实例aa
aa:display()                --相当于aa.display(aa),aa继承了Account的display方法
-- 输出22
print(aa.display)           --function: 00D0B4A0
print(Account.display)      --function: 00D0B4A0
-- aa.display()会出错,因为aa调用的是Account的display方法,有一个用冒号隐藏的self参数
aa.display(aa) --这样写也可以
-- 输出22
-- 有人说,调用Lua类的属性使用点号,调用其方法使用冒号,其实指的就是这个意思

-- 子类还可以重载父类方法
function aa:display()
    print("override father method display!")
end
aa:display()                -- 运行时输出override father method display!
print(aa.display)           --function: 001E7EB0,可以看到函数与父类函数地址不同

参考:
http://www.open-open.com/lib/view/open1479280061023.html
http://www.jellythink.com/archives/511
http://dhq.me/lua-learning-notes-metatable-metamethod
http://www.bbsmax.com/A/KE5QAW05LG/