一、基础概念 

Lua 本身是函数式的语言,但借助 metatable (元表)这个强大的工具,Lua 实现操作符重载易如反掌。。

下文将详细的解释在Lua中实现类的原理,涉及到的细节点将拆分出来讲,相信对Lua中实现类的理解有困难的同学将会释疑。

类是什么?

  想要实现类,就要知道类到底是什么。在我看来,类,就是一个自己定义的变量类型。它约定了一些它的属性和方法,是属性和方法的一个集合。所有的方法都需要一个名字,即使是匿名函数实际上也有个名字。这就形成了方法名和方法函数的键值映射关系,即方法名为键,映射的值为方法函数。

比如说有一个类是人,人有一个说话的方法,那就相当于,人(Person)是一个类,说话(talk)是它的一个方法名,说话函数是它的实际说话所执行到的内容。人也有一个属性,比如性别,性别就是一个键(sex),性别的实际值就是这个键所对应的内容。

理解了类实际上是一个键值对的集合,我们不难想到用Lua中自带的表来实现类。

实例是什么?

  如果理解了类实际就是一个键值映射的表,那么我们再来理解实例是什么。实例就是具有类的属性和方法的集合,也是一个表了。听起来好像和类差不多?类全局只有一个集合,相当于上帝,全局只有一块内存;而实例就普通了,普天之下有那么多人,你可以叫A说一句话,A便执行了他的说话方法,但是不会影响B的说话。因为他们是实例,彼此分配着不同的内存。说了那么多废话,其实实例就是由类创建出来的值,试着把类想象成类型而不是类。

两个语法糖

试着创建一个人类 Person

Person = {name="这个人很懒"}

以上代码将Person初始化为一个表,这个表拥有一个为name的键,其默认值是"这个人很懒"。说成白话就是人类拥有一个叫名字的属性。那就再赋予人类一个说话的功能吧。

Person.talk = function(self, words)
print(self.name.."说:"..words)
end

以上代码在Person表中加入一个键值对,键为talk,值为一个函数。好了,只要调用,Person.talk(Person, "你好"),将会打印出:这个人很懒说:你好

不过在写程序时,大家都习惯把function放在前面,这就是函数的语法糖:

function Person.talk(self, words)
print(self.name.."说:"..words)
end

  这与上面的函数定义是等价的,但是这么写你就很难看出来talk其实是Person表中的一个键,其对应的值为一个函数。当然嘴巴都是长在自己身上的,说话只能自己说,不可能自己张嘴别人说话,所以每次都传个self参数实在是有点不美观,于是冒号语法糖上场。

我们还可以这么定义人类的说话功能:

function Person:talk(words)
print(self.name.."说:"..words)
end

  这与上面两段代码都是等价的,它的变化是少了self的参数,将点Person.talk改为了冒号Person:talk。但是函数体内,却依然可以使用self,在使用:代替.时,函数的参数列表的第一个参数不再是words,Lua会自动将self做为第一个参数。这个self参数代表的意思就是这个函数的实际调用者。所以我们调用Person:talk("你好")与Person.talk(Person, "你好")是等价的,这就是冒号语法糖带来的便利。

如何查找表中的元素?下面我们需要理解在Lua的表中是怎么查找一个键所对应的值的。假设我们要在表p中查找talk这个键所对应的值,请看下面的流程图:

p中有没有talk这个键? 有 --> 返回talk对应的值
|
没有
|
p中是否设置过metatable? 否 --> 返回nil
|

|
在p的metatable中有没有__index这个键? 没有 --> 返回nil
|

|
在p的metatable中的__index这个键对应的表中有没有talk这个键? 没有 --> 返回nil
|
有,返回getmetatable(p).__index.talk

理解以上内容是本文的重点,反复阅读直至你记住了。可以看到,由于metatable和__index这两个神奇的东西,Lua能在当前表中不存在这个键的时候找到其返回值。 下面将会讲一讲metatable这个语言特性。

对metatable的理解

  metatable是什么?metatable的中文名叫做元表。它不是一个单独的类型,元表其实就是一个表。我们知道在Lua中表的操作是有限的,例如表不能直接相加,不能进行比较操作等等。元表的作用就是增加和改变表的既定操作。只有设置过元表的表,才会受到元表的影响而改变自身的行为。通过全局方法setmetatable(t, m),会将表t的元表设置为表m。通过另一个全局方法getmetatable(t)则会返回它的元表m。

  注意:所有的表都可以设置元表,然而新创建的空表如果不设置,是没有元表的。

元方法

元表作为一个表,可以拥有任意类型的键值对,其真正对被设置的表的影响是Lua规定的元方法键值对。这些键值对就是Lua所规定的键,比如前面说到的__index,__add,__concat等等。这些键名都是以双斜杠__为前缀。其对应的值则为一个函数,被称为元方法(metamethod),这些元方法定义了你想对表自定义的操作。

例如:前面所说的__index键,在Lua中它所对应的元方法执行的时机是当查找不存在于表中的键时应该做的操作。考虑以下代码:

--定义元表m
m = {}
--定义元表的__index的元方法
--对任何找不到的键,都会返回"undefined"
m.__index = function ( table, key )
return "undefined"
end --表pos
pos = {x=, y=}
--初始没有元表,所以没有定义找不到的行为
--因为z不在pos中,所以直接返回nil
print(pos.z) -- nil
--将pos的元表设为m
setmetatable(pos, m)
--这是虽然pos里仍然找不到z,但是因为pos有元表,
--而且元表有__index属性,所以执行其对应的元方法,返回“undefined”
print(pos.z) -- undefined

  pos表中本没有z这个键,通过设置pos的元表为m,并设置m__index对应的方法,这样所有取不到的键都会返回“undefined”了。以上我们了解到,元表的__index属性实际上是给表配备了找不到键时的行为。

注意:元表的__index属性对应的也可以为一个表。

再举个例子,希望能够加深对元表和元方法的理解,__add键,考虑以下代码:

--创建元表m,其中有__add键和其定义的方法
local m = {
__add = function(t1, t2)
local sum = {}
for key, value in pairs(t1) do
sum[key] = value
end for key, value in pairs(t2) do
if sum[key] then
sum[key] = sum[key] + value
else
sum[key] = value
end
end
return sum
end
} --将table1和table2都设置为m
local table1 = setmetatable({, , }, m)
local table2 = setmetatable({, , }, m) --表本来是不能执行 + 操作的,但是通过元表,我们做到了!
for k, v in pairs(table1 + table2) do
print(k, v)
end
--print
--1 23
--2 25
--3 27

表本身是不能用+连起来计算的,但是通过定义元表的__add的方法,并setmetatable到希望有此操作的表上去,那些表便能进行加法操作了。

因为元表的__add属性是给表定义了使用+号时的行为。

类的实现手段

  好,假设前面的内容你都没有疑问的阅读完毕话,我们开始进入正题。请先独立思考一会,我们该怎么去实现一个Lua的类?种种铺垫后,我们的类是一个表,它定义了各种属性和方法。我们的实例也是一个表,然后我们类作为一个元表设置到实例上,并设置类的__index值为自身。

例如人类:

--设置Person的__index为自身
Person.__index = Person --p是一个实例
local p = {} --p的元表设置为Person
setmetatable(p, Person) p.name = "路人甲" --p本来是一个空表,没有talk这个键
--但是p有元表,并且元表的__index属性为一个表Person
--而Person里面有talk这个键,于是便执行了Person的talk函数
--默认参数self是调用者p,p的name属性为“路人甲”
p:talk("我是路人甲") --于是得到输出
--路人甲说:我是路人甲

为了方便,我们给人类一个创建函数create:

function Person:create(name)
local p = {}
setmetatable(p, Person)
p.name = name
return p
end local pa = Person:create("路人甲")
local pb = Person:create("路人乙")
pa:talk("我是路人甲") --路人甲说:我是路人甲
pb:talk("我是路人乙") --路人乙说:我是路人乙
这样我们可以很方便用Person类创建出pa和pb两个实例,这两个实例都具备Person的属性和方法。以上便是Lua实现一个类的方法,至于类的继承,当成一次练习吧,请大家思考~
 

二、实践  

(1) 实现类的构造、析构和初始化。

-- 使用lua自定义类
local _class = {} function BaseClass()
-- 生成一个类类型, 实际上存放类信息
local class_type = {} -- new 一个对象
class_type.New = function(...)
print(" >>>>>>>>>>> class_type.New")
-- 生成一个类对象
local obj = {} -- 注册一个delete方法(类似与C++里面的析构函数)
obj.DeleteMe = function()
print(" >>>>>>>>>>>>>> obj:DeleteMe -- g_mylayer:DeleteMe")
end -- new创建一个对象,就会执行类的初始化方法(从最顶层父类到最底层子类,依次调用定义的__init函数),采用可变参数
class_type.__init(obj, ...)
-- class_type.__init(...) return obj
end
return class_type
end -------------------------------------
-- 以下为测试上面的类创建函数
-------------------------------------
mylayer = mylayer or BaseClass()
-- 初始化函数
function mylayer:__init()
print(" >>>>>>>>>> mylayer:__init")
end
-------------------------------------
-- New创建对象,New函数在调用BaseClass创建类之后被定义
print(" >>>>>>>>>> mylayer.New1")
g_mylayer = mylayer.New()
print(" >>>>>>>>>> mylayer.New2")
-------------------------------------
g_mylayer:DeleteMe() -- 或者 g_mylayer.DeleteMe() -- output
>>>>>>>>>> mylayer.New1
>>>>>>>>>>> class_type.New
>>>>>>>>>> mylayer:__init
>>>>>>>>>> mylayer.New2
>>>>>>>>>>>>>> obj:DeleteMe -- g_mylayer:DeleteMe
[Finished in .0s]

(2)在上面的BaseClass方法中,我们可以在方法里面调用mylayer:xxxx()形式的函数,如函数"mylayer:__init()"可以在方法里面这样调用“class_type.__init(obj, ...)”,但却无法通过New出来的对象进行调用,也即无法通过对象g_mylayer调用,“g_mylayer:__init()”,因为对象没有这个方法,故而会报nil错误:

attempt to call field '__init' (a nil value)

如果要实现在类中的方法也可以通过对象调用,那么需要使用到本文第一部分概念中提到的lua的元表和元方法; 如果是新添加的方法,那么原来类和对象中都没有此方法, 我们当然也希望,新添加的方法也可以自动的加入到类中供调用. 可以对上面程序作修改,使之支持对象调用类方法:

-- 使用lua自定义类
local _class = {} function BaseClass()
-- 生成一个类类型, 实际上存放类信息
local class_type = {} -- 创建接口(类似与C++里面的构造函数)
class_type.New = function(...)
print(" >>>>>>>>>>> class_type.New")
-- 生成一个类对象
local obj = {} -- 支持类中的方法可以通过new出来的对象obj进行调用, 当调用到obj对象中不存在的方法的时候, 通过__index指示搜索类和对象共有的vtbl表
setmetatable(obj, { __index = _class[class_type] }) -- 注册一个delete方法(类似与C++里面的析构函数)
obj.DeleteMe = function()
print(" >>>>>>>>>>>>>> obj:DeleteMe -- g_mylayer:DeleteMe")
end -- new创建一个对象,就会执行类的初始化方法(从最顶层父类到最底层子类,依次调用定义的__init函数),采用可变参数
class_type.__init(obj, ...) return obj
end ------------------------------
local vtbl = {}
_class[class_type] = vtbl
-- 当搜索vtbl找不到方法时,则在vtbl中创建。
setmetatable(class_type, { __newindex = function(t,k,v)
vtbl[k] = v
end,
-- 搜索vtbl
__index = vtbl,
}) return class_type
end -------------------------------------
-- 以下为测试上面的类创建函数
-------------------------------------
mylayer = mylayer or BaseClass()
-- 初始化函数
function mylayer:__init()
print(" >>>>>>>>>> mylayer:__init")
end -- 新函数
function mylayer:newFunc1()
print(" >>>>>>>>>> mylayer:newFunc1")
end
function mylayer:newFunc2()
print(" >>>>>>>>>> mylayer:newFunc2")
end
-------------------------------------
-- New创建对象,New函数在调用BaseClass创建类之后被定义
print(" >>>>>>>>>>>>>>>>>> new方法调用__init")
g_mylayer = mylayer.New()
print(" >>>>>>>>>>>>>>>>>> 通过对象调用")
g_mylayer:__init()
print(" >>>>>>>>>>>>>>>>>> new3")
-- 如果是新添加的方法,则创建之
-- 通过类直接调用
mylayer:newFunc1()
-- 通过对象调用
g_mylayer:newFunc2() -- output
 >>>>>>>>>>>>>>>>>> new方法调用__init
 >>>>>>>>>>> class_type.New
 >>>>>>>>>> mylayer:__init
 >>>>>>>>>>>>>>>>>> 通过对象调用
 >>>>>>>>>> mylayer:__init
 >>>>>>>>>>>>>>>>>> new3
 >>>>>>>>>> mylayer:newFunc1
 >>>>>>>>>> mylayer:newFunc2
[Finished in 0.0s]

  注:上面创建的类中定义了一个新函数MyTestOutput,并使用New出来的对象g_mylayer进行调用,之所以能通过对象进行类函数调用,是因为创建对象的New方法里面,把对象的元表设置为相应的类,这样当在对象(对象表)中找不到调用的方法时,会去寻找类中定义的方法,找到了则调用,如果找不到,则创建之。

(3)这样代码就支持了对象调用类方法,不过对于类,如果父类有一个方法newMethod(),那么创建的子类应该要具有这个方法而不用重新定义,也即,如果在子类中找不到newMethod()方法的时候,会自动到父类中搜索. 继续对上面代码进行完善,使之支持方法的继承:

-- 使用lua自定义类
local _class = {} function BaseClass(super)
-- 生成一个类类型, 实际上存放类信息
local class_type = {} -- 指示父类
-- class_type.super = super -- 创建接口(类似与C++里面的构造函数)
class_type.New = function(...)
print(" >>>>>>>>>>> class_type.New")
-- 生成一个类对象
local obj = {} -- 对象类型赋值(内省)
-- obj._class_type = class_type -- 支持类中的方法可以通过new出来的对象obj进行调用, 当调用到obj对象中不存在的方法的时候, 通过__index指示搜索类和对象共有的vtbl表
setmetatable(obj, { __index = _class[class_type] }) -- 注册一个delete方法(类似与C++里面的析构函数)
obj.DeleteMe = function()
print(" >>>>>>>>>>>>>> obj:DeleteMe -- g_mylayer:DeleteMe")
end -- new创建一个对象,就会执行类的初始化方法(从最顶层父类到最底层子类,依次调用定义的__init函数),采用可变参数
class_type.__init(obj, ...) return obj
end ------------------------------
local vtbl = {}
_class[class_type] = vtbl
-- 当搜索vtbl找不到方法时,则在vtbl中创建。
setmetatable(class_type, { __newindex = function(t,k,v)
vtbl[k] = v
end,
-- 搜索vtbl
__index = vtbl,
}) -- 有父类,vtbl搜索父类
if super then
setmetatable(vtbl, {__index =
-- 搜索函数,针对父类向上 一层层搜索
function(t,k)
local ret = _class[super][k]
return ret
end
})
end return class_type
end ------------------------------------------------------------------
-- 测试类的继承
------------------------------------------------------------------
-- baselayer 基类,没有父类
baselayer = baselayer or BaseClass()
function baselayer:baseMethod()
print(" >>>>>>>>>>>>>>> baselayer:baseMethod")
end driverlayer = driverlayer or BaseClass(baselayer)
-- 派生类调用父类方法
driverlayer:baseMethod()
-- output
>>>>>>>>>>>>>>> baselayer:baseMethod
[Finished in 0.0s]

  注:上面添加了对super父类的支持,并对析构函数__delete进行完善,添加函数体和顶层类的析构处理。至此,上面的类创建函数已经支持了类的创建、初始化、构造和析构、对象调用类函数、从父类继承、继承父类的方法、在顶层父类中往下调用各个子类的函数(如__init()函数,在父类的__init()函数中调用子类的__init()函数)。

最后添加了对于self关键字的使用,具体可以参见《lua程序设计(第二版)》

lua中类的实现原理和实践的更多相关文章

  1. 搞懂分布式技术9:Nginx负载均衡原理与实践

    搞懂分布式技术9:Nginx负载均衡原理与实践 本篇摘自<亿级流量网站架构核心技术>第二章 Nginx负载均衡与反向代理 部分内容. 当我们的应用单实例不能支撑用户请求时,此时就需要扩容, ...

  2. Redis原理与实践总结

    Redis原理与实践总结 本文主要对Redis的设计和实现原理做了一个介绍很总结,有些东西我也介绍的不是很详细准确,尽量在自己的理解范围内把一些知识点和关键性技术做一个描述.如有错误,还望见谅,欢迎指 ...

  3. 新书介绍 -- 《Redis核心原理与实践》

    大家好,今天给大家介绍一下我的新书 -- <Redis核心原理与实践>. 后端开发的同学应该对Redis都不陌生,Redis由于性能极高.功能强大,已成为业界非常流行的内存数据库. < ...

  4. Redis核心原理与实践--Redis启动过程源码分析

    Redis服务器负责接收处理用户请求,为用户提供服务. Redis服务器的启动命令格式如下: redis-server [ configfile ] [ options ] configfile参数指 ...

  5. Redis核心原理与实践--事务实践与源码分析

    Redis支持事务机制,但Redis的事务机制与传统关系型数据库的事务机制并不相同. Redis事务的本质是一组命令的集合(命令队列).事务可以一次执行多个命令,并提供以下保证: (1)事务中的所有命 ...

  6. Redis、Zookeeper实现分布式锁——原理与实践

    Redis与分布式锁的问题已经是老生常谈了,本文尝试总结一些Redis.Zookeeper实现分布式锁的常用方案,并提供一些比较好的实践思路(基于Java).不足之处,欢迎探讨. Redis分布式锁 ...

  7. Atitit 管理原理与实践attilax总结

    Atitit 管理原理与实践attilax总结 1. 管理学分类1 2. 我要学的管理学科2 3. 管理学原理2 4. 管理心理学2 5. 现代管理理论与方法2 6. <领导科学与艺术4 7. ...

  8. Atitit.ide技术原理与实践attilax总结

    Atitit.ide技术原理与实践attilax总结 1.1. 语法着色1 1.2. 智能提示1 1.3. 类成员outline..func list1 1.4. 类型推导(type inferenc ...

  9. Atitit.异步编程技术原理与实践attilax总结

    Atitit.异步编程技术原理与实践attilax总结 1. 俩种实现模式 类库方式,以及语言方式,java futuretask ,c# await1 2. 事件(中断)机制1 3. Await 模 ...

随机推荐

  1. OCILIB开源的C/C++ Oracle驱动

    OCILIB是一个开源的.跨平台的Oracle驱动,可以高效的操作Oracle数据库. OCILIB库: - 提供丰富的.全特性的.容易使用的API - 运行在所有的Oracle平台 - 使用标准的I ...

  2. 一款基于jquery ui的动画提交表单

    今天要给大家分享一款基于jquery ui的动画提交表单.这款提交表单的的效果是以动画的形式依次列表所需填写的信息.效果非常不错,效果图如下: 在线预览   源码下载 实现的代码. html代码: & ...

  3. 一款jquery实现的整屏切换特效

    今天要为大家带来一款由jquery实现的整屏切换特效,在右侧有圆型小标,每点一个切换一屏.当然,你也可以滚动鼠标来切换页面.效果非常好.我们看下效果吧 在线预览   源码下载 html代码: < ...

  4. 简单5步,释放Mac磁盘空间

    收藏一下,以备后用 http://ourmacs.com/mactech/340

  5. Kettle转换或作业乱码

    结果这样做还是乱码,其实这和

  6. C语言 · 前10名

    算法提高 前10名   时间限制:1.0s   内存限制:256.0MB      问题描述 数据很多,但我们经常只取前几名,比如奥运只取前3名.现在我们有n个数据,请按从大到小的顺序,输出前10个名 ...

  7. ubuntu 16.04 上opengl 的安装以及例子程序编译执行

    因为最近在移植 Qt5.7 + opengl , 遇到了难以越过的山峰,没有办法,试着在 ubuntu 16.04上将 opengl 配置以下,记录: 安装相关的库: sudo apt-get ins ...

  8. uboot中CMD的实现

    CMD配置位于config_cmd_default.h   configs/at91/sam9g10ek.h 头文件位于include/command.h 41 struct cmd_tbl_s {  ...

  9. 关于Cocos2d-x节点和精灵节点的坐标、位置以及大小的设置

    1.cocos2d-X中的坐标(0,0),就是运行框的左下角位置,所以运行框看起来就是一个第一象限. 2.节点的锚点就是我们setPosition所设定的位置,默认锚点是在节点的中心,也就是setPo ...

  10. C struct的内存对齐

    说明:如果你看到了这篇,请略过. struct是复合类型. 其中的成员在内存中的分布都是对齐的. 这个对齐的意思是,struct的sizeof运算结果必定是其最大类型长度的整数倍. --注意,如果st ...