Lua面向对象编程

在lua原生语法特性中是不具备面向对象设计的特性。因此,要想在lua上像其他高级语言一样使用面向对象的设计方法有以下两种选择:一种是使用原生的元表(metatable)来模拟面向对象设计,另外一种则是使用第三方框架LuaScriptoCore来实现。下面将逐一讲解这两种方式的实现过程(以下内容将基于Lua 5.3版本进行阐述)。

1. 元表方式

1.1 关于元表(metatable)

在lua中每种类型变量都可以有一个元表,而元表实际上是一个table,它用于定义原始值在特定操作下的行为。如果想改变一个变量在特定操作下的行为,则可以在它的元表中设置对应元方法(metamethod)。换种说法,元表就是一个变量钩子,用来钩取变量的底层处理方法(即元方法),然后改写这些方法的处理行为。

元表原理示意图

其中元方法如下面表格所示:

元方法 说明
__index 当访问变量某个key时,如果没有对应的value,则会访问元表__index元方法所指定的对象。如果指定值为table类型,则会访问该table的key所对应的值;如果指定值为function类型,则该方法返回值作为对应key的值
__newindex 当设置变量的某个key时,如果没有对应的key,则会访问元表__newindex元方法来处理键值设置
__add 当两个变量进行加法操作时触发,如:var1 + var2
__sub 当两个变量进行减法操作时触发,如:var1 - var2
__mul 当两个变量进行乘法操作时触发,如:var1 * var2
__div 当两个变量进行除法操作时触发,如:var1 / var2
__mod 当两个变量进行取模操作时触发,如:var1 % var2
__unm 当变量进行取反操作时触发,如:~var
__pow 当变量进行幂操作时触发,如:var^2
__concat 当两个变量进行连接时触发,如:var1 .. var2
__eq 当两个变量判断是否相等时触发,如:var1 == var2
__lt 当一个变量判断是否小于另一个变量时触发,如:var1 < var2
__le 当一个变量判断是否小于或等于另一个变量时触发,如:var1 <= var2`
__call 当变量被用作方法调用时触发,一般来说function类型是允许被调用的,对于其他类型默认是不能进行调用的,那么该元方法的作用就是让你的变量能够像function一样被调用
__tostring 当使用tostring转换变量为字符串或者调用print进行打印时触发,如:local t = {}; print (t);
__gc 当变量被回收时触发。
__mode 当设置table为弱引用table时使用。弱引用table可以让保存的key或者value为弱引用状态,方便GC标记和回收(如果非弱引用情况下,必须要table被回收时,其内部的key和value才允许GC回收)。

元表的设置基本分为三个步骤:

  1. 先创建一个作为元表的table
  2. 设置需要实现的元方法。
  3. 使用setmetatable方法将元表绑定到变量中。

实现代码如下:

-- 创建元表并设置元方法
local mt = {};
mt.__index = function (table, key)

  return "Hello Metatable!";

end

-- 创建实例并绑定元表
local t = {};
setmetatable(t, mt);

上面的元方法在本篇文章中不会一一细说,在后面的章节会针对面向对象需要使用的__index__newindex__call__gc__tostring这几个元方法进行举例说明。

为了帮助大家理解如何实现lua的面向对象,下面的章节会逐步地构建面向对象所需要的特性,完整地演示整个演化过程。废话不多说,直接开干~

1.2 类型声明

在开始构建类型前,我们先为面向对象设想一些基本的规则,这样可以避免后面参与扩展和开发的人因为理解的不一样,导致整个结构的规则混乱和不一致。根据需要我们先设定如下几点:

  • 类型名称首字母必须大写
  • 类型必须为全局的变量
  • 类型必须使用__index元方法指向自身
  • 类型必须使用__gc元方法进行销毁时的操作
  • 类型的属性必须使用点语法进行声明和访问
  • 类型的类方法和实例方法声明和调用必须使用冒号(:)语法声明,目的让方法都带有一个默认的self参数
  • 类型的构造方法命名为create,并且为类方法

根据上面的约定,我们先来定义一个所有对象的基类Object

-- 声明类型
Object = {
   };

-- 设置__index元方法
Object.__index = Object;

-- 设置__gc元方法
Object.__gc = function (instance)
  -- 进行对象销毁工作
  print(instance, "destroy");
end

-- 定义构造函数
function Object:create() 

  local instance = {
   };
  setmetatable(instance, self);
  return instance;  

end

-- 定义实例方法
function Object:toString()

  print (tostring(self));

end

可以看到我们创建了一个全局的table变量Object来作为类型。

然后使用了元方法__index进行自身指向,这样做的目的是使实例对象能够访问对象所定义的属性或者方法。因为__index的特点是当变量访问指定不存在的key时,就会去调用其元表的__index方法,由于__index指向就是Object,因此就会判断Object是否存在该key,并进行返回。另外如果指向的对象也设置了元表并且使用了__index,那么会继续寻找其元表的__index指向,直到最终没有设置元表的对象(这个特性在实现继承时特别关键,并且效果很好)。具体找寻方式如下图所示:

__index处理流程示意图

元方法__gc在这里也使用到了,利用其特性可以轻松地知道对象实例销毁时机,可以在方法里面进行一些后续的处理,类似C++中的析构函数。在这里只是简单地打印是哪个对象被销毁。

接着我们讲解一下构造方法create的实现,方法中调用了setmetatable方法将self(即类型Object)元表绑定到instance这个变量,配合之前设置__index元方法,实例变量就会拥有与Object相同的一些属性和方法定义了。

toString方法则是一个实例方法, 主要用于将对象转换成字符串。

该类型具体使用方法如下所示:

local obj = Object:create();
print(obj:toString());

冒号(:)是lua的语法糖,其等效写法为Object.create(Object)obj.toString(obj),省略了把obj自身作为参数传入到方法中的这个步骤,让整个调用看起来更像是obj自身提供的方法。

1.3 添加属性声明

类型的定义少不了属性和方法的声明,上例中只进行了方法的声明,这节将会重点讲述属性如何进行声明。利用上面的例子,我们再给Object增加一个属性。

-- 声明属性
Object.tag = 999;

很简单,这样就完成了一个简单的属性定义。在代码中可以这样使用:

local obj  = Object:create();
print (obj.tag);      -- 输出999

但是问题来了,在面向对象中,其实类型和实例应该都会拥有属性,即类属性和实例属性。那么如果直接在Object中定义属性是没有办法区分这是类属性还是实例属性的。所以,这里借鉴了javascript中的<

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值