开始

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
endfunction Account:deposit (v)self.balance = self.balance + v
endfunction Account:new (o)o = o or {}   -- create object if user does not provide onesetmetatable(o, self)self.__index = selfreturn 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()

个人认为,官网上这种方案实现的继承并不优雅。用惯了面向对象的语言,看见此段代码的第一反应是:SpecialAccountAccount类的一个对象,而不是一个派生类。

对于常在面向对象语言和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 = classNametableNewClass.super = supertableNewClass.category = 'Class'tableNewClass.Ctor = falsetableNewClass.Dtor = falsetableNewClass.__index = supersetmetatable(tableNewClass, tableNewClass)--创建新对象tableNewClass.New = function(...)local tableNewObject = {type = tableNewClass,category = 'Instance'}tableNewObject.__index = tableNewClasssetmetatable(tableNewObject, tableNewObject)do--递归调用父类的构造函数local CreateCreate = function(class, ...)if type(class.super) == 'table' then Create(class.super, ...) endif type(class.Ctor) == 'function' then class.Ctor(tableNewObject, ...) endendCreate(tableNewClass, ...)end--销毁对象tableNewObject.Dispose = function(self)--依次调用所有父类的析构函数local currentClass = self.typewhile currentClass ~= nil doif type(currentClass.Dtor) == 'function' then currentClass.Dtor(self) endcurrentClass = currentClass.superendendreturn tableNewObjectendtableClassNames[className] = tableNewClassreturn tableNewClass
endClass.New = New
return Class

简单讲解下代码:

  1. 通过Class.New方法来创建一个类,为保证全局的唯一性,需要对每个类提供唯一的名字;同时可以指定该类的父类以实现继承。
  2. 通过newClass.New方法来创建一个newClass的实例,创建该实例时,将会依次递归调用基类的构造函数(Ctor)。
  3. 通过newClassInstance.Dispose方法可以销毁newClassInstance实例,销毁该实例时,将会依次递推调用基类的析构函数(Dtor)。
  4. 代码中,为类和实例增加了标识性字段category,用来区分一个对象是类还是实例。

接下来看看如何使用:

Class = require("MetatableClass")ClassA = Class.New('ClassA')ClassA.Ctor = function(self)print 'Ctor in ClassA'
endClassA.Dtor = function(self)print 'Dtor in ClassA'
endClassA.Print = function(self)print 'Print in ClassA'
endClassB = Class.New('ClassB', ClassA)ClassB.Ctor = function(self)print 'Ctor in ClassB'
endClassB.Dtor = function(self)print 'Dtor in ClassB'
endb = 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' thenreturn innerObjectelseif temp[innerObject] ~= nil thenreturn temp[innerObject]endlocal newObject = {}temp[innerObject] = newObjectfor k, v in pairs(innerObject) donewObject[CloneProcess(k)] = CloneProcess(v)endreturn setmetatable(newObject, getmetatable(innerObject))endreturn CloneProcess(object)
end

这种方案简单、粗暴,不存在逐层查找的问题,但是会带来新的问题,即每一个实例都会拥有类的完整的字段集,从而造成内存的浪费。我通常会在不需要创建大量实例时使用该方案。

为了折中解决以上两种方案带来的问题,下面提供另一种实现方案。

Metatable+Clone——折中的方案

这种方案来自云风的博客,原文可以在云风的BLOG中可以找到。

贴上原始代码:

local _class={}function class(super)local class_type={}class_type.ctor=falseclass_type.super=superclass_type.new=function(...) local obj={}dolocal createcreate = function(c,...)if c.super thencreate(c.super,...)endif c.ctor thenc.ctor(obj,...)endendcreate(class_type,...)endsetmetatable(obj,{ __index=_class[class_type] })return objendlocal vtbl={}_class[class_type]=vtblsetmetatable(class_type,{__newindex=function(t,k,v)vtbl[k]=vend})if super thensetmetatable(vtbl,{__index=function(t,k)local ret=_class[super][k]vtbl[k]=retreturn retend})endreturn class_type
end

代码的基本思想是:

  1. 通过全局的class方法创建一个类。
  2. 为每一个类创建一个基础表(class_type)用来存放一些基础的字段和方法,同时创建一个配套的虚表(vtbl),用来存储继承来的字段和方法,并为class_typevtbl构建索引关系。
  3. 通过类的new方法可以创建一个实例。在创建实例时递归调用父类的构造函数(ctor)。
  4. 改写实例的__index行为,使对实例的索引转向对class_type对应的vtbl的索引。
  5. 改写class_type__newindex来拦截对class_type的赋值,将值赋给class_type对应的vtbl
  6. 改写vtbl__index行为,当vtbl中不存在目标字段时,尝试从父类(super)的vtbl_super中索引,并将索引结果存放复制到vtbl,这样下一次再索引相同字段时,就无需再从super中索引了。

这种方案很好地折中了以上两个问题:效率和内存问题,并且提供了实现面向对象的另一种思路。

总结

上述的几种方案是常用的方案,项目中往往需要根据具体的需求采用特殊的设计。比如有些类字段不应该被修改(如类名、父类),就可以通过修改__newindex行为来阻止对这些字段的修改。

上述几个方案只是起个抛砖引玉的作用,如果有看客有其他好的实现方式,欢迎留言探讨。

Lua——Lua中的面向对象相关推荐

  1. Cocos2d-x 脚本语言Lua中的面向对象

    Cocos2d-x 脚本语言Lua中的面向对象 面向对象不是针对某一门语言,而是一种思想.在面向过程的语言也能够使用面向对象的思想来进行编程. 在Lua中,并没有面向对象的概念存在,没有类的定义和子类 ...

  2. Lua(Codea) 中 table.insert 越界错误原因分析

    2019独角兽企业重金招聘Python工程师标准>>> Lua(Codea) 中 table.insert(touches, touch.id, touch) 越界错误原因分析 背景 ...

  3. Lua 语言中的点、冒号与self

    lua编程中,经常遇到函数的定义和调用,有时候用点号调用,有时候用冒号调用,这里简单的说明一下原理. girl = {money = 200} function girl.goToMarket(gir ...

  4. Win32下 Qt与Lua交互使用(二):在Lua脚本中使用Qt类

    话接上篇.成功配置好Qt+Lua+toLua后,我们可以实现在Lua脚本中使用各个Qt的类.直接看代码吧. #include "include/lua.hpp" #include ...

  5. Lua虚拟机中的数据结构与栈

    Lua虚拟机中的数据结构与栈 来源 https://blog.csdn.net/zry112233/article/details/80828327 由上一篇文章可知解释器分析Lua文件之后生成Pro ...

  6. Lua语言中的冒号:和点.

    lua编程中,经常遇到函数的定义和调用,有时候用点号调用,有时候用冒号调用. girl = {money = 200} function girl.goToMarket(girl ,someMoney ...

  7. 使用ToLua插件 关于Lua脚本中 require 添加模块经常报错找不到Lua文件的问题

    Lua的require添加模块经常报错,找不到 LuaException: E:/UnityProJect/Calculator/Assets/Script/Lua/NpcManage.lua:4: ...

  8. python 提取lua文件中的中文

    #-*- coding: UTF-8 -*-import os# 遍历指定目录,显示目录下的所有文件名 def eachFile(filepath):for root,dirs,files in os ...

  9. LUA———Lua和C 区别

    1.lua和c有两种关系: 一种是在lua中调用C的函数,C称为库代码,一种是C中调用lua,C就称为应用程序代码,此时C中包含了lua的解释器.注意在C++中,通常要把lua的一些头文件定义在ext ...

最新文章

  1. Akka的字数统计MapReduce
  2. 华为手机应用鸿蒙os,华为手机内置应用逐渐向鸿蒙 OS 靠拢
  3. 南漂DBA——除了996,还可以收获这些...
  4. nginx解析漏洞 只要可以上传文件就会被黑
  5. postman怎么传对象list_postman 传递json的参数里面带了List对象
  6. 列表生成式的复习以及生成器的练习, 杨辉三角实例(非常巧妙)
  7. 电灯泡实验应该怎么做_英文论文润色应该怎么做
  8. 图像频域增强:傅里叶变换
  9. 64位电脑上安装MySQL进行MFC开发的相关问题
  10. iOS音频掌柜-- AVAudioSession
  11. 一个完整机器学习项目的基本流程
  12. 如何用深度学习对几种类型的图片进行分类(tensorflow,CNN)
  13. 利用pm2 启动node项目
  14. 产品原型设计:使用axure实现菜单下拉效果
  15. 树莓派 3B+/4B 连接“手机热点“或“WiFi“ 后无法上网(必解)
  16. 使用ffmpeg调整图像大小
  17. 又发福利!日历小程序源码
  18. IMU:姿态解算算法集合
  19. 注册流程(分离HLR/HSS)
  20. iOS Core Bluetooth_4 用作中央设备的常用方法(2/2)[swift实现]

热门文章

  1. 华为手机访问文件服务器,手机访问云服务器文件
  2. 喜报|探码科技荣获2022年度四川省“专精特新”中小企业认定
  3. 国防科技大学计算机学院窦强,国防科技大学
  4. 简单易懂:iPhone USB PD快充从入门到精通(转)
  5. 第11集 关于库卡机器人 FB PSPS
  6. 如何阻止华为杀应用_华为手机“杀”后台严重受不了?别慌,这些小技巧就能轻松搞定...
  7. OC 技术 (需要UniversalLink)第三方微信(登录,分享,支付)详解(手动集成)(视频教学)
  8. js 将Unix时间戳转换为普通日期格式
  9. 当爱你的人不再爱你了,还有AI“安慰”你
  10. c语言中double的用法,c语言中double的用法