写lua这么久了,也知道怎么样用lua来实现所谓的面向对象。下面这段代码是我常用来new一个新实例对象的:

local Object = {a = 123}
function Object:new (data)  
    local data = data or {}
    setmetatable(data, {__index = self})   
    return data   
end 
local o = Object:new()

以上代码有没有问题呢,咋一看是没有什么问题,利用元表实现了对象的继承,凡是新实例都会拥有Object的方法。但是前几天在实现一段代码时发现了一个严重的问题,新的实例共享了Object的数据,这种情况肯定是不允许出现的,因为实例压根不是独立的实例了。之前也一直没有注意到这个问题,因为这个问题比较隐蔽。比方说上面的代码,改变o.a = 456,是不会影响Object.a的,但是如果Object = {a = {b=123}},生成新实例o后,执行o.a.b = 456,Object对象的数据也跟着变了,后面新生成的实例数据都变了。

这是为什么呢?因为前面在执行o.a = 456时,他是赋值操作,他会给自己的字段a赋值为456,这里并不是索引到Object字段。而o.a.b = 456有个获取字段a的过程,o本身没有,就从Object中去查找,改变的是Object中a.b的值。所以后面生成实例的值都将是改变之后的值,正确的做法是:

function table.deepcopy(t, nometa)
    local lookup_table = {}
    local function _copy(t,nometa)
        if type(t) ~= "table" then
            return t
        elseif lookup_table[t] then
            return lookup_table[t]
        end
        local new_table = {}
        lookup_table[t] = new_table
        for index, value in pairs(t) do
            new_table[_copy(index)] = _copy(value)
        end
        if not nometa then
           new_table = setmetatable(new_table, getmetatable(t))
        end
        return new_table
    end
    return _copy(t)
end

function Object:new2 (data)  
    data = data or {}
    local copy = table.deepcopy(self)
    setmetatable(data, {__index = copy})   
    return data   
end

在生成元表的索引时指向的是一个深度拷贝的table,这样就不会指向原来的值了,该共享函数的还是会共享函数。所以这才是面向对象生成新实例正确的写法。

虽然达到了目的,我还是不建议大家这么写,首先效率是一个要考虑的因素,在我这篇文章lua代码优化中有一个对比试验,可以看出这种写法效率会大打折扣。其次过多的引入面向对象的东西,在lua这种解释型语言甚至所有脚本语言中都是不太可取的,增加了代码的复杂性和不可维护性。

所以建议用lua的table来封装函数就好,不要模拟生成新实例的方式来表示新的数据结构。你可能会问,我真的需要表示多个相同的实例怎么办。这样的需求时刻存在,但是你可以改变你的思路,用函数的方式来达到目的。这样你需要改变函数的设计方式,尽可能的用参数,返回值来影响函数的输出,而不是使用全局变量或所谓的成员变量。这样相当于把函数当成一个黑盒,相同的输入总会得到相同的输出。这也是函数式编程语言中所提倡的,函数是第一等,first-class。比如在过去设计棋牌游戏中,一张桌子里面有四把椅子,我会考虑会面向对象的方式来生成四个实例,每个实例中有各种属性等字段。现在我只会用到一个数据结构table,用它来封装各种操作函数,然后用传入的参数来区分不同的椅子即可。