> Lua 的面向对象
Lua 里除去基本的类型 number, boolean, string 外,最常用的类型就是 table 。table 可以充当基本的容器(如 list, map)以外,Lua 的很多特性包括OO都是基于 table 的操作而实现的。
在 Lua 中没有 class 这个概念,要实现OO式的编程,可以利用 table 来进行模拟实现,如 :
-- define
Hero = {}
Hero.attack = function(self, target)
print(self.name .. " attack " .. target.name)
end
Hero.new = function(name)
local hero = {}
hero.name = name
hero.attack = function(target)
Hero.attack(hero, target)
end
reutrn hero
end
-- execute
local heroA = Hero('A')
local heroB = Hero('B')
heroA.attack(heroB)
可见,利用这种方式可以做到与大多数语言一样的面向对象的书写风格。但是,这里有一个 不爽的地方—— Hero 想要添加对外的 api 的话,需要在生成 table 实例的地方显示地写一次函数封装用以隐藏把实例对象作为第一个参数传参的行为,这里不仅仅明摆了多一次函数调用,而且还不便于后来者想要扩展此类的 api 。
事实上,Lua 本身是有提供机制可以表现出OO风格的 —— 使用 metatable 。
> Metatable
metatable 称作元表,其本质上是一个 table ,主要是用于集合那些用户定义的数据行为。可以通过 Lua 两个内建的函数 getmetatable 和 setmetatable 来对数据进行元表的操作。
- setmetatable( t , mt ) —— 对一个数据 t 进行 mt 元表的绑定,这个数据 t 本身必须要是 table 类型的变量
- setmetatable( var ) —— 获取一个数据其绑定的元表
执行 :
local mt = getmetatable("hello world")
print("mt type : " .. type(mt))
print("elements ...")
for k, v in pairs(mt) do
print (k, v)
end
结果 :
由代码执行结果可见,metatable 本身确实为一个 table,其中 __index 字段对应的值也是一个 table 对象。
再执行 :
local str = "hello world"
for k, v in pairs(getmetatable(str)["__index"]) do
print(k, v)
end
结果 :
可以看出来, __index 表里面存储的都是 string 对象方法,它的功能事实上就相当于 python 的默认的 __getattr__ 方法。
metatable 里面除了可以定义 __index 意外,还可以定义__sub(相减) ,__add(相加) ... 这样的类似于 c++ 重载 operator 的行为。
> Lua 模块
在 lua 运行时里,会有一个 table 来保存一些全局的变量(这里我们暂且称之为 “env_table” )。env_table 所保存的环境信息,我们可以通过全局的 _G 来进行访问。
运行 :
for k, v in pairs(_G) do
print (k, v)
end
结果 :
可以看到一些内置常用的模块或函数都列出来了。当然,如果你定义了一个全局变量,也会在这列表结果中展示。
当我们编写一个模块文件时,我们通常都会在文件一开始写上 module(...) 或 module(... , package.seeall) 。module 函数表示当前文件为一个模块,除了会将当前模块加入到全局的加载模块(package.loaded)以外,还会为模块文件内预设一些预定义变量 :
- _M : table,用于表示当前模块自身的一些信息,包含当前文件作用域内的函数与变量,类似 _G
- _NAME : 当前模块名信息
- _PACKAGE : 包的信息
因此,当调用 module(... , package.seeall) 时,相当于执行了 setmetatable( _M, { __index : _G }) ,因此我们于模块范围内,就能访问到所有通过 _G 能访问到的模块或者变量了。
最后,回到最开始的问题 —— “lua 的面向对象”。既然当前模块内的函数信息都存储在了 _M 中,那么当我们生成返回一个 table 对象的时候 ,完全可以通过 setmetatable 的方式来让 table 对象继承当前这个 _M ,从而达到让外面调用table 对象时能直接访问到模块内定义的公共函数的目的。结合最开始的例子,代码如下 :
-- define
moudle(...)
Hero = {__index = _M}
function attack(self, target)
print(self.name .. " attack " .. target.name)
end
function new(name)
local hero = {}
hero.name = name
reutrn setmetatable(hero, Hero)
end
-- execute
local heroA = Hero('A')
local heroB = Hero('B')
heroA:attack(heroB)
可见,代码更简洁清晰了,唯一的区别就只是在调用对象的方法时,改用了 “:” 这个特殊的语法而已。当我们调用一个 string 变量的方法时,也可以写成诸如 strvar:sub(),strvar:find() ... 的形式,可见 string 类型其内部定义的方式本质上也是如出一辙的。