开始
Lua本身并不是面向对象的语言、不存在类的概念。Lua官网16.1 – Classes中有如下描述。
Lua does not have the concept of class
但我们可以在Lua中来实现面向对象。在Lua中实现面向对象的方式有很多种,本篇挑比较常见的几种方案进行解析。
Metatable和Metamethod——官方的方案
首先明确一点:表(Table)是Lua中的基本数据类型之一,也是Lua中唯一的数据结构。我们将要实现的类本质上也是一张表。Lua官网11 – Data Structures
Tables in Lua are not a data structure; they are the data structure. All structures that other languages offer—arrays, records, lists, queues, sets—are represented with tables in Lua. More to the point, tables implement all these structures efficiently.
之后再来看看Metatable和Metamethod。官方的解释可以参考Lua官网13 – Metatables and Metamethods
其中下面这句话为面向对象提供了思路。
Any table can be the metatable of any other table; a group of related tables may share a common metatable (which describes their common behavior); a table can be its own metatable (so that it describes its own individual behavior). Any configuration is valid.
看看官方在这种思路下实现的Account类:
Account = {balance = 0}
function Account:withdraw (v)
self.balance = self.balance - v
end
function Account:deposit (v)
self.balance = self.balance + v
end
function Account:new (o)
o = o or {} -- create object if user does not provide one
setmetatable(o, self)
self.__index = self
return o
end
Account类提供了new方法,用来创建一个Account类的对象。
方法内部先构建一张表o,并将其Metatable和元方法(Metamethod)__index均设置为self。这里将__index设置为一张表是Lua语法上的便利,它的本质还是一个函数(Method),具体参照Lua官网。
当我们调用a = Account:new()创建一个新Account对象时,new方法里的self就指向Account。也就是说,现在a的Metatable和__index元方法均指向Account,那么就实现了一个简单的继承。
接下来调用a:deposit(100),Lua在表a中找不到deposit,于是会先看它有没有Metatable,如果有,再看它的Metatable有没有__index元方法或__index对应的表。这二者缺一不可。
最后的结果当然是查到了deposit方法,于是代码顺利执行,通过打印a.balance可以看见此时a.balance已经为100了。
官方还提供了继承的实现方案来实现从Account派生出SpecialAccount类。
SpecialAccount = Account:new()
个人认为,官网上这种方案实现的继承并不优雅。用惯了面向对象的语言,看见此段代码的第一反应是:SpecialAccount是Account类的一个对象,而不是一个派生类。
对于常在面向对象语言和Lua之间切换的人,这很容易令人困惑。因此,下面不再对官方的例子进行深入剖析。而是利用官方的思路来重新进行面向对象的设计。
Metatable和Metamethod——改进方案
在面向对象语言中,类(Class)和实例(Instance)是不同的概念。因此,在Lua中实现面向对象,我也会将Class和Instance分开。
完整的代码如下:
--MetatableClass.lua
local Class = {}
--类名,保存“类名-类”记录
local tableClassNames = {}
local function New(className, super)
assert(type(className) == 'string' and #className > 0)
assert(tableClassNames[className] == nil, 'Try to redefine a class with name : [' .. className .. ']')
local tableNewClass = {}
tableNewClass.className = className
tableNewClass.super = super
tableNewClass.category = 'Class'
tableNewClass.Ctor = false
tableNewClass.Dtor = false
tableNewClass.__index = super
setmetatable(tableNewClass, tableNewClass)
--创建新对象
tableNewClass.New = function(...)
local tableNewObject = {
type = tableNewClass,
category = 'Instance'
}
tableNewObject.__index = tableNewClass
setmetatable(tableNewObject, tableNewObject)
do
--递归调用父类的构造函数
local Create
Create = function(class, ...)
if type(class.super) == 'table' then Create(class.super, ...) end
if type(class.Ctor) == 'function' then class.Ctor(tableNewObject, ...) end
end
Create(tableNewClass, ...)
end
--销毁对象
tableNewObject.Dispose = function(self)
--依次调用所有父类的析构函数
local currentClass = self.type
while currentClass ~= nil do
if type(currentClass.Dtor) == 'function' then currentClass.Dtor(self) end
currentClass = currentClass.super
end
end
return tableNewObject
end
tableClassNames[className] = tableNewClass
return tableNewClass
end
Class.New = New
return Class
简单讲解下代码:
- 通过
Class.New方法来创建一个类,为保证全局的唯一性,需要对每个类提供唯一的名字;同时可以指定该类的父类以实现继承。 - 通过
newClass.New方法来创建一个newClass的实例,创建该实例时,将会依次递归调用基类的构造函数(Ctor)。 - 通过
newClassInstance.Dispose方法可以销毁newClassInstance实例,销毁该实例时,将会依次递推调用基类的析构函数(Dtor)。 - 代码中,为类和实例增加了标识性字段
category,用来区分一个对象是类还是实例。
接下来看看如何使用:
Class = require("MetatableClass")
ClassA = Class.New('ClassA')
ClassA.Ctor = function(self)
print 'Ctor in ClassA'
end
ClassA.Dtor = function(self)
print 'Dtor in ClassA'
end
ClassA.Print = function(self)
print 'Print in ClassA'
end
ClassB = Class.New('ClassB', ClassA)
ClassB.Ctor = function(self)
print 'Ctor in ClassB'
end
ClassB.Dtor = function(self)
print 'Dtor in ClassB'
end
b = ClassB.New()
b.Print()
b:Dispose()
最后输出如下:

这是一种简单、易懂的方案,可以应付大部分场合。但是,当继承关系过深,逐层去索引将会带来效率问题。这个问题也是接下来要解决的问题。
Clone——粗暴的方案
为了避免逐层索引带来的效率问题,这里提供了Clone的方案来解决此问题。
与上一种方案不同,假设有一个类ClassA,当需要从ClassA创建实例时,我们可以将ClassA的所有字段通通复制进一张新表(新实例)。可以通过如下的Clone方法来完成此操作:
local function Clone(object)
local temp = {}
local function CloneProcess(innerObject)
if type(innerObject) ~= 'table' then
return innerObject
elseif temp[innerObject] ~= nil then
return temp[innerObject]
end
local newObject = {}
temp[innerObject] = newObject
for k, v in pairs(innerObject) do
newObject[CloneProcess(k)] = CloneProcess(v)
end
return setmetatable(newObject, getmetatable(innerObject))
end
return CloneProcess(object)
end
这种方案简单、粗暴,不存在逐层查找的问题,但是会带来新的问题,即每一个实例都会拥有类的完整的字段集,从而造成内存的浪费。我通常会在不需要创建大量实例时使用该方案。
为了折中解决以上两种方案带来的问题,下面提供另一种实现方案。
Metatable+Clone——折中的方案
这种方案来自云风的博客,原文可以在云风的BLOG中可以找到。
贴上原始代码:
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
代码的基本思想是:
- 通过全局的
class方法创建一个类。 - 为每一个类创建一个基础表(
class_type)用来存放一些基础的字段和方法,同时创建一个配套的虚表(vtbl),用来存储继承来的字段和方法,并为class_type和vtbl构建索引关系。 - 通过类的
new方法可以创建一个实例。在创建实例时递归调用父类的构造函数(ctor)。 - 改写实例的
__index行为,使对实例的索引转向对class_type对应的vtbl的索引。 - 改写
class_type的__newindex来拦截对class_type的赋值,将值赋给class_type对应的vtbl。 - 改写
vtbl的__index行为,当vtbl中不存在目标字段时,尝试从父类(super)的vtbl_super中索引,并将索引结果存放复制到vtbl,这样下一次再索引相同字段时,就无需再从super中索引了。
这种方案很好地折中了以上两个问题:效率和内存问题,并且提供了实现面向对象的另一种思路。
总结
上述的几种方案是常用的方案,项目中往往需要根据具体的需求采用特殊的设计。比如有些类字段不应该被修改(如类名、父类),就可以通过修改__newindex行为来阻止对这些字段的修改。
上述几个方案只是起个抛砖引玉的作用,如果有看客有其他好的实现方式,欢迎留言探讨。
本文介绍在Lua中实现面向对象的多种方法,包括使用Metatable和Metamethod的基础方案,以及改进后的Class和Instance分离方案,还有高效但内存消耗大的Clone方案,最后提出了一种结合Metatable和Clone的折中方案。
1万+

被折叠的 条评论
为什么被折叠?



