木匣子

Web/Game/Programming/Life etc.

Lua 简单面向对象模型

Lua 使用 table 来模拟各种数据结构,当然,也可以用它来模拟类(Class)。要实现类的模拟,我们需要找到一种方式来定义类,并从类创建实例,实现继承等。

0x00 Meta Table & Mate Methods

在 Lua 中,可以通过定制 table 的元表以及元方法来改变 table 的行为,例如实现运算符重载等。其中有一个很有用的元方法: __index

当访问一个 table 的属性时它是这样工作的:

  1. table 中是否有这个属性?Y) 返回该属性的值;
  2. 元表是否为空?Y)返回 nil;
  3. __index 是否为空? Y)返回 nil
  4. __index 是一个 function?Y)调用 __index(table, key) 并返回结果
  5. __index 是一个 table?Y)递归从 __index 指向的表中查询该属性
foo = {}
foo.bar -- nil

bar = {}
setmetatable(bar, {
    __index = {
        baz = 1
    }
})
bar.baz -- 1

递归查表这个行为是不是很像 javascript 的原型链!如果把 Lua 的 table 比作是 Javascript 的 Object,那么元表中的 __index 就相当于 Object 的 __proto__。与 Javascript 不同的是, Lua 中的 function 与 table 是两个不同的类型;而 Javascript 中的 Function 同时也是(is-a) Object 。

0x01 Class

作为一个类,它应该有一个构造函数(constructor),并且能实例化对象(new)。这些对象能使用类提供的方法:

Foo = {}
Foo.__index = Foo

function Foo:ctor(str)
    self.str = str
end

function Foo:say()
    print(self.str)
end

function Foo.new(...)
    local instance = {}
    setmetatable(instance, Foo)
    instance:ctor(...)
    return instance
end

foo = Foo.new("bar")
foo:say() -- output: bar

Foo.new 方法为实例创建了个空表,并将 Foo 作为它的元表。借由 Foo.__index = Foo 这个实例能够访问到 Foo 中定义的方法。

根据这个方法,我们可以创建一个类工厂:

function class()
    local cls = {}
    cls.__index = cls

    -- a dumb ctor
    function cls:ctor() end

    function cls.new(...)
        local instance = setmetatable({}, cls)
        instance:ctor(...)
        return instance
    end

    return cls
end

然后就可以用这个类工厂批量创建类了:

Foo = class()

-- override ctor
function Foo:ctor(str)
    self.str = str
end

function Foo:say()
    print(self.str)
end

foo = Foo.new("bar")
foo:say() -- output: bar

0x02 Inheritance

接下来我们可以给类增加继承的功能。所谓继承,即子类能够扩展超类,其实例能够使用该类以及超类的方法。

本质上就是使用元表的递归查找,不断向上检索属性和方法:

function class(super)
    local cls
    if super == nil then
        -- table with a dumb ctor
        cls = { ctor = function() end }
    else
        cls = setmetatable({}, super)
        cls.super = super
    end

    cls.__index = cls

    function cls.new(...)
        local instance = setmetatable({}, cls)
        instance:ctor(...)
        return instance
    end

    return cls
end

试试效果:

Foo = class()

-- override ctor
function Foo:ctor(str)
    self.str = str
end

function Foo:say()
    print(self.str)
end

Bar = class(Foo)

function Bar:sayMore(str)
    self.super.say(self) -- invoke method from super class
    print(str)
end

bar = Bar.new("bar")
bar:sayMore("bar") -- output: bar bar

0x03 is-a

为了让继承更有说服力,我们可以参考 javascript 的 instanceof 运算符 原理实现 is-a 方法,用来检测实例与类的关系——若一个类是一个 table 的元表,则这个 table 是这个类的实例,并向上递归:

function isa(instance, Class)
    local metatable = getmetatable(instance)
    while metatable ~= nil do
        if metatable == Class then
            return true
        else
            metatable = getmetatable(metatable)
        end
    end

    return false
end

测试:

Foo = class()
foo = Foo.new()
print(isa(foo, Foo)) -- true

Bar = class(Foo)
bar = Bar.new()
print(isa(bar, Bar)) -- true
print(isa(bar, Foo)) -- true

Baz = class(Foo)
baz = Baz.new()
print(isa(baz, Bar)) -- false
print(isa(baz, Foo)) -- true

0x04 More

说到面向对象,除了继承,还得提封装和多态。在 Lua 中,使用闭包(Closure)很容易就可以实现封装。而上面提到的 is-a 方法,对多态提供了很好的支持。作为一个轻量级脚本语言,虽然不能面面具到,但已经有了实现 OOP 的基础。