Lua的面向对象
Lua语言本身并没有提供面向对象的语法机制,这需要我们自己设计实现一套类的机制。首先,对于面向对象来说,我们至少需要类和对象这两个概念。同时,类至少包含一个用于构造对象的方法。对应到Lua上,就是一个代表类的table,它有一个构造函数,返回代表该类对象的table:
Class = {}
function Class:New()
local o = {}
return o
end
local inst = Class:New()
另外,一个类可以定义若干的方法,该类的所有对象都可以调用这个方法。可是返回的对象table和类table实际上并无关联,如何让对象table索引到类table里定义的方法呢?答案是使用metatable。利用__index
属性,当对象table找不到方法时,就会去索引类table里的方法:
Class = {}
function Class:New()
local o = {}
setmetatable(o, {__index = self})
return o
end
function Class:A()
print("hello world!")
end
local inst = Class:New()
inst:A()
运行可以看到hello world正常输出了。
一个类除了定义方法以外,还可以定义一些成员变量,每个对象拥有各自的成员变量,是相互独立的。另外,我们不希望一个类对象可以随便定义新的成员变量或者成员函数。同样地,这里也是用到了metatable,__index
属性可以让对象table去索引类table里的成员变量,利用__newindex
属性,我们可以对要写入table的key进行检查,当对象想定义不在类table中存在的成员,或者覆盖类table中存在的函数时,可以进行报错提示;而覆盖已经存在于类的成员变量,则在对象table中写入一份新的副本,保证不同对象table的成员变量互相独立:
Class = {}
function Class:New()
self.var = 1
local o = {}
setmetatable(o,
{
__index = self,
__newindex = function(t, k, v)
if self[k] then
if type(self[k]) ~= "function" then
rawset(t, k, v)
else
print("overriding function in object is forbidden")
end
else
print("declaring new var/function in object is forbidden")
end
end
})
return o
end
function Class:A()
print("hello world!")
end
local a = Class:New()
local b = Class:New()
print(a.var, b.var)
a.var = 2
print(a.var, b.var)
b.var = 3
print(a.var, b.var)
a.newvar = 1
b.A = function()
print("object hello world")
end
print(a.newvar)
b:A()
运行输出的结果如下:
既然有了成员变量,那么在面向对象的机制中,还有一个很重要的访问控制,即我们需要实现private机制,来使得某些成员变量或者成员函数只在这个类内部可见,对象引用它是不可见的。私有函数是比较好实现的,毕竟一个类的所有对象共享一个函数,我们只需要定义该函数为local function,对象就无法访问到它了:
local function PrivateFunc(self)
print("private func ", self.var)
end
function Class:B()
PrivateFunc(self)
end
local a = Class:New()
a:B()
a:PrivateFunc()
运行后会报错提示,找不到PrivateFunc这个函数调用,这个函数只能在类的内部进行调用,对象是访问不到的,也就实现了私有函数。
但是对于私有变量来说,实现起来却比较困难,如果我们按照私有函数的方式来做,那么会出现类的所有对象都引用着同一个local变量,虽然对象无法访问到变量本身,但是这个变量一旦发生变化,将会影响到所有对象。它实际上变成了一个static的private变量,这个不是我们想要的。
也许我们想说,可以通过操纵metatable的__index,来禁止对象外部访问私有变量。虽然这样行得通,但是同样的问题没有解决,即这个变量还是一个static变量。
为了让每个对象都拥有各自的私有变量,我们只能将私有变量塞到Class:New()方法中。但是这就引发了另一个问题:就是这个私有变量定义之后,只在Class:New()方法中可见,其他方法也访问不到这个私有变量了。因此,我们还需要将类的其他成员函数的定义全部挪到New方法中,也就是说,从原先所有的对象通过metatable来共享一份成员函数,变成了所有的对象都拥有自己的成员函数:
local Class = {}
function Class:New()
local o = {}
o.var = 1
local pvar = 1
function o:A()
print("hello world!")
end
function o:setPrivate(newval)
pvar = newval
end
function o:getPrivate()
return pvar
end
setmetatable(o,
{
__newindex = function(t, k, v)
if self[k] then
if type(self[k]) ~= "function" then
rawset(t, k, v)
else
print("overriding function in object is forbidden")
end
else
print("declaring new var/function in object is forbidden")
end
end
})
return o
end
local a = Class:New()
local b = Class:New()
a.pvar = 1
b.pvar = 1
a:setPrivate(3)
b:setPrivate(2)
print(a:getPrivate())
print(b:getPrivate())
输出如下:
两个对象都无法直接访问到私有变量,而且它们各自拥有各自的私有变量,互不干扰。
接下来,让我们考虑类的继承。由于我们现在实际上有两种实现方式,一种是使用metatable,实现的思路其实也比较直观,就是当类本身找不到方法或者变量时,就往它的父类寻找:
local Class = {}
function Class:New(base)
self.var = 1
setmetatable(self,
{
__index = base,
})
local o = {}
setmetatable(o,
{
__index = self,
__newindex = function(t, k, v)
if self[k] then
if type(self[k]) ~= "function" then
rawset(t, k, v)
else
print("overriding function in object is forbidden")
end
else
print("declaring new var/function in object is forbidden")
end
end
})
return o
end
function Class:A()
print("hello world!")
end
local Extend = {}
function Extend:New(base)
-- 注意这里显式传入了self,因为需要的是扩展类信息而不是基类信息
local o = base and base.New(self) or {}
return o
end
function Extend:A()
print("hello from extend")
end
一种是使用闭包,这种情况下,new出来的对象已经包含了这个类的所有信息,那就初始化的时候直接构造的是基类对象而不是空table即可:
local Extend = {}
function Extend:New(base)
local o = base and base:New() or {}
function o:A()
print("hello from extend")
end
return o
end
以上讨论的是单继承的情况,那么多继承呢?对于使用metatable的方式,比较简单,直接修改__index方法,让其遍历所有基类,直到查找到第一个符合的为止:
function CreateClass(...)
local class = {}
local baseList = { ... }
setmetatable(class,
{
__index = function(_, k)
for _, base in ipairs(baseList) do
local v = base[k]
if v then
return v
end
end
end,
})
function class:New()
local o = {}
setmetatable(o,
{
__index = self,
__newindex = function(t, k, v)
if self[k] then
if type(self[k]) ~= "function" then
rawset(t, k, v)
else
print("overriding function in object is forbidden")
end
else
print("declaring new var/function in object is forbidden")
end
end
})
return o
end
return class
end
local Base1 = CreateClass()
function Base1:A()
print("hello world from base 1")
end
local Base2 = CreateClass()
function Base2:B()
print("hello world from base 2")
end
local Base3 = CreateClass(Base1, Base2)
local a = Base3:New()
a:A()
a:B()
对于使用闭包的方式,却有点麻烦,因为这种方式类本身是不包含任何具体信息的,需要在调用New方法的时候,先构造出基类对象,将对象里的信息复制填充;不仅如此,由于我们要抽象出一个通用方法,定义类具体的方法,成员只能通过外部参数的形式进行传递:
function CreateClass(template, ...)
local class = {}
local baseList = { ... }
function class:New()
local o = {}
for _, base in ipairs(baseList) do
local bo = base:New()
for k, v in pairs(bo) do
if not o[k] then
o[k] = v
end
end
end
for k, v in pairs(template) do
o[k] = v
end
setmetatable(o,
{
__newindex = function(t, k, v)
if self[k] then
if type(self[k]) ~= "function" then
rawset(t, k, v)
else
print("overriding function in object is forbidden")
end
else
print("declaring new var/function in object is forbidden")
end
end
})
return o
end
return class
end
local Base1 = CreateClass({ A = function() print("hello world from base 1") end })
local Base2 = CreateClass({ B = function() print("hello world from base 2") end })
local Base3 = CreateClass({}, Base1, Base2)
local a = Base3:New()
a:A()
a:B()
这种方式,用起来其实有点复杂了。
云风在博客中提到了另外一种lua实现面向对象的做法:
local _class={}
function class(super)
local class_type={}
class_type.ctor=false
class_type.super=super
class_type.new=function(...)
local obj={}
do
local create
create = function(c,...)
if c.super then
create(c.super,...)
end
if c.ctor then
c.ctor(obj,...)
end
end
create(class_type,...)
end
setmetatable(obj,{ __index=_class[class_type] })
return obj
end
local vtbl={}
_class[class_type]=vtbl
setmetatable(class_type,{__newindex=
function(t,k,v)
vtbl[k]=v
end
})
if super then
setmetatable(vtbl,{__index=
function(t,k)
local ret=_class[super][k]
vtbl[k]=ret
return ret
end
})
end
return class_type
end
这里有个小小的优化,就是把访问过的字段缓存到vtbl中,避免频繁调用元方法。
如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路)-