【菜狗教程】Lua.02 - 元表和面向对象的模拟

962 阅读4分钟

菜狗教程 —— Lua 篇 02

上一篇概述了 Lua 的基本语法,距离实际上使用 Lua 还有很长的距离,第二篇我们就直接上一个大台阶,看看 Lua 的高级用法。

从 table 开始

表(table) 可以说是 Lua 最重要的数据类型,不仅功能丰富,还有很多巧妙的设计,绝对让人眼前一亮。

  • table 支持 . 运算符,t.key 等价于 t[key]。
  • table 支持 : 运算符,t:func() 等价于 t.func(t)。函数体内部可以用 self 访问 t.

元表(metatable)

meta 这个词在编程中非常常见,顾名思义就可以推测出元表的功能:修改表的行为

举个例子,一般来说访问表中不存在的 key 无法取到值,Lua 的 table 确实会返回 nil。如果把这个值当成是 function 类型调用,程序就会报错。

t = {}

-- t.mFuncAdd = function(a, b)
--     return a + b
-- end

print(t.mFuncAdd(1, 2))

如果没有注释掉函数定义,输出结果应该是 3,现在的实际运行结果是一个异常:

lua_table_nil_run.png

如果 mFuncAdd 的赋值不在我们可以控制的范围里,又不想程序因此停止,我们可以通过元表修改表的行为,比如让这个表访问不到的 key 默认返回一个空函数替代 nil。

t = {}

-- Lua 中通过 `setmetatable(table, metatable)` 函数给一个表设定元表。
setmetatable(t, {
    __index = function(t, key)
        return function() end
    end
})

print(t.mFuncAdd(1, 2))

lua_table_nilfunc_run.png

成功了,我们通过修改元表的 __index 函数修改了表在找不到 key 时的行为。

此处有一个容易迷惑的点,元表也是一个普通的表,所以每个表都有 __index 字段。要修改 的行为,必须修改 它的元表 的 __index 才有效,修改它自己的 __index 是没效果的。

元表支持修改的字段和对应功能如下表:

元方法说明
__add重载加法操作符 +
__sub重载减法操作符 -
__mul重载乘法操作符 *
__div重载除法操作符 /
__mod重载取模操作符 %
__pow重载幂运算操作符 ^
__unm重载一元减运算符 -
__concat重载连接运算符 ..
__len重载 # 操作符
__eq重载等于运算符 ==
__lt重载小于运算符 <
__le重载小于等于运算符 <=
__index重载索引访问操作符 []
__newindex重载赋值操作符 []=
__call重载函数调用操作符 ()
__tostring将表转换为字符串

表 + 函数 = 对象

Lua 虽然并不经常独立使用,但嵌入其他语言动态化的部分通常也会包含比较复杂的业务逻辑,毕竟特别简单的逻辑可以通过一些开关实现更简洁的动态化。对于复杂的业务逻辑,使用面向对象的编程思想往往更易于理解,Lua 的实际使用中经常使用 table + function 来模拟对象。

看一个简单的例子:

local obj = { name = "MyObject" }
function obj:toString()
    print("name is "..self.name)
end

-- test
print(obj.name)
print(obj:toString())

lua_table_oop_run.png

我们的目的是模拟一个叫做 Obj 的类,添加了一个成员变量 name 和一个方法 toString。从使用的代码上看,无论是创建对象还是访问成员变量、调用方法,都和面向对象的代码有些相似。

在通过 Lua 动态化一部分业务逻辑的时候,Lua 的「对象」来自其他语言中预构造的表,更进一步可能是业务中一个对象的映射,通过这样的模拟对象,Lua 中的代码会更贴近业务代码,易于理解。

继承和多态

说到面向对象,显然上面的模拟还不够。众所周知,面向对象编程有封装、继承、多态、抽象的特性,模拟对象最多就是做了简单的封装。

第二节的元表并不是乱入的,Lua 通过元表的能力也可以模拟继承和多态。更详细点说,Lua 可以通过修改元表的 __index 实现继承,可以通过表自己定义同名函数覆写元表函数实现多态。

这次我们直接看代码吧,都是前面的知识结合:

-- Base Object
local Obj = { name = "Obj" }
Obj.__index = Obj

function Obj:new(name)
    local self = setmetatable({}, Obj)
    self.name = name
    return self
end

function Obj:toString()
    return self.name
end

local mObj = Obj:new("A")
print(mObj:toString())

-- Person extends Obj
local Person = Obj:new("Obj")
Person.__index = Person

function Person:new(name, age)
    local self = setmetatable({}, self)
    self.name = name
    self.age = age
    return self
end

local person = Person:new('菜狗', 2)
print(person:toString()) -- Obj:toString()

-- override toString
function Person:toString()
    return '{name='..self.name..', age='..self.age..')'
end

print(person:toString()) -- Person:toString()

Person 继承自 Obj,可以直接调用父类的 toString,也可以重写 toString 函数实现自己的逻辑。

lua_oop_run.png

彩蛋环节

lua_egg_meta_func.png

image.png