Lua的面向对象

Lua语言本身并没有提供面向对象的语法机制,这需要我们自己设计实现一套类的机制。首先,对于面向对象来说,我们至少需要类和对象这两个概念。同时,类至少包含一个用于构造对象的方法。对应到Lua上,就是一个代表类的table,它有一个构造函数,返回代表该类对象的table:

Class = {}
function Class:New()
    local o = {}
    return o
end

local inst = Class:New()

另外,一个类可以定义若干的方法,该类的所有对象都可以调用这个方法。可是返回的对象table和类table实际上并无关联,如何让对象table索引到类table里定义的方法呢?答案是使用metatable。利用__index属性,当对象table找不到方法时,就会去索引类table里的方法:

Class = {}
function Class:New()
    local o = {}
    setmetatable(o, {__index = self})
    return o
end
function Class:A()
    print("hello world!")
end
local inst = Class:New()
inst:A()

运行可以看到hello world正常输出了。

一个类除了定义方法以外,还可以定义一些成员变量,每个对象拥有各自的成员变量,是相互独立的。另外,我们不希望一个类对象可以随便定义新的成员变量或者成员函数。同样地,这里也是用到了metatable,__index属性可以让对象table去索引类table里的成员变量,利用__newindex属性,我们可以对要写入table的key进行检查,当对象想定义不在类table中存在的成员,或者覆盖类table中存在的函数时,可以进行报错提示;而覆盖已经存在于类的成员变量,则在对象table中写入一份新的副本,保证不同对象table的成员变量互相独立:

Class = {}
function Class:New()
    self.var = 1
    local o = {}
    setmetatable(o,
    {
        __index = self,
        __newindex = function(t, k, v)
            if self[k] then
                if type(self[k]) ~= "function" then
                    rawset(t, k, v)
                else
                    print("overriding function in object is forbidden")
                end
            else
                print("declaring new var/function in object is forbidden")
            end
        end
    })
    return o
end
function Class:A()
    print("hello world!")
end
local a = Class:New()
local b = Class:New()
print(a.var, b.var)
a.var = 2
print(a.var, b.var)
b.var = 3
print(a.var, b.var)
a.newvar = 1
b.A = function()
    print("object hello world")
end
print(a.newvar)
b:A()

运行输出的结果如下:

既然有了成员变量,那么在面向对象的机制中,还有一个很重要的访问控制,即我们需要实现private机制,来使得某些成员变量或者成员函数只在这个类内部可见,对象引用它是不可见的。私有函数是比较好实现的,毕竟一个类的所有对象共享一个函数,我们只需要定义该函数为local function,对象就无法访问到它了:

local function PrivateFunc(self)
    print("private func ", self.var)
end

function Class:B()
    PrivateFunc(self)
end

local a = Class:New()
a:B()
a:PrivateFunc()

运行后会报错提示,找不到PrivateFunc这个函数调用,这个函数只能在类的内部进行调用,对象是访问不到的,也就实现了私有函数。

但是对于私有变量来说,实现起来却比较困难,如果我们按照私有函数的方式来做,那么会出现类的所有对象都引用着同一个local变量,虽然对象无法访问到变量本身,但是这个变量一旦发生变化,将会影响到所有对象。它实际上变成了一个static的private变量,这个不是我们想要的。

也许我们想说,可以通过操纵metatable的__index,来禁止对象外部访问私有变量。虽然这样行得通,但是同样的问题没有解决,即这个变量还是一个static变量。

为了让每个对象都拥有各自的私有变量,我们只能将私有变量塞到Class:New()方法中。但是这就引发了另一个问题:就是这个私有变量定义之后,只在Class:New()方法中可见,其他方法也访问不到这个私有变量了。因此,我们还需要将类的其他成员函数的定义全部挪到New方法中,也就是说,从原先所有的对象通过metatable来共享一份成员函数,变成了所有的对象都拥有自己的成员函数:

local Class = {}

function Class:New()
    local o = {}
    o.var = 1
    local pvar = 1

    function o:A()
        print("hello world!")
    end

    function o:setPrivate(newval)
        pvar = newval
    end

    function o:getPrivate()
        return pvar
    end

    setmetatable(o,
    {
        __newindex = function(t, k, v)
            if self[k] then
                if type(self[k]) ~= "function" then
                    rawset(t, k, v)
                else
                    print("overriding function in object is forbidden")
                end
            else
                print("declaring new var/function in object is forbidden")
            end
        end
    })

    return o
end

local a = Class:New()
local b = Class:New()
a.pvar = 1
b.pvar = 1
a:setPrivate(3)
b:setPrivate(2)
print(a:getPrivate())
print(b:getPrivate())

输出如下:

两个对象都无法直接访问到私有变量,而且它们各自拥有各自的私有变量,互不干扰。

接下来,让我们考虑类的继承。由于我们现在实际上有两种实现方式,一种是使用metatable,实现的思路其实也比较直观,就是当类本身找不到方法或者变量时,就往它的父类寻找:

local Class = {}
function Class:New(base)
    self.var = 1

    setmetatable(self,
    {
        __index = base,
    })

    local o = {}
    setmetatable(o,
    {
        __index = self,
        __newindex = function(t, k, v)
            if self[k] then
                if type(self[k]) ~= "function" then
                    rawset(t, k, v)
                else
                    print("overriding function in object is forbidden")
                end
            else
                print("declaring new var/function in object is forbidden")
            end
        end
    })
    return o
end

function Class:A()
    print("hello world!")
end

local Extend = {}
function Extend:New(base)
    -- 注意这里显式传入了self,因为需要的是扩展类信息而不是基类信息
    local o = base and base.New(self) or {}
    return o
end

function Extend:A()
    print("hello from extend")
end

一种是使用闭包,这种情况下,new出来的对象已经包含了这个类的所有信息,那就初始化的时候直接构造的是基类对象而不是空table即可:

local Extend = {}
function Extend:New(base)
    local o = base and base:New() or {}
    function o:A()
        print("hello from extend")
    end
    return o
end

以上讨论的是单继承的情况,那么多继承呢?对于使用metatable的方式,比较简单,直接修改__index方法,让其遍历所有基类,直到查找到第一个符合的为止:

function CreateClass(...)
    local class = {}

    local baseList = { ... }
    setmetatable(class,
    {
        __index = function(_, k)
            for _, base in ipairs(baseList) do
                local v = base[k]
                if v then
                    return v
                end
            end
        end,
    })

    function class:New()
        local o = {}
        setmetatable(o,
        {
            __index = self,
            __newindex = function(t, k, v)
                if self[k] then
                    if type(self[k]) ~= "function" then
                        rawset(t, k, v)
                    else
                        print("overriding function in object is forbidden")
                    end
                else
                    print("declaring new var/function in object is forbidden")
                end
            end
        })
        return o
    end

    return class
end

local Base1 = CreateClass()
function Base1:A()
    print("hello world from base 1")
end

local Base2 = CreateClass()
function Base2:B()
    print("hello world from base 2")
end

local Base3 = CreateClass(Base1, Base2)

local a = Base3:New()
a:A()
a:B()

对于使用闭包的方式,却有点麻烦,因为这种方式类本身是不包含任何具体信息的,需要在调用New方法的时候,先构造出基类对象,将对象里的信息复制填充;不仅如此,由于我们要抽象出一个通用方法,定义类具体的方法,成员只能通过外部参数的形式进行传递:

function CreateClass(template, ...)
    local class = {}
    local baseList = { ... }

    function class:New()
        local o = {}
        for _, base in ipairs(baseList) do
            local bo = base:New()
            for k, v in pairs(bo) do
                if not o[k] then
                    o[k] = v
                end
            end
        end
    
        for k, v in pairs(template) do
            o[k] = v
        end
    
        setmetatable(o,
        {
            __newindex = function(t, k, v)
                if self[k] then
                    if type(self[k]) ~= "function" then
                        rawset(t, k, v)
                    else
                        print("overriding function in object is forbidden")
                    end
                else
                    print("declaring new var/function in object is forbidden")
                end
            end
        })
    
        return o
    end

    return class
end

local Base1 = CreateClass({ A = function() print("hello world from base 1") end })
local Base2 = CreateClass({ B = function() print("hello world from base 2") end })
local Base3 = CreateClass({}, Base1, Base2)

local a = Base3:New()
a:A()
a:B()

这种方式,用起来其实有点复杂了。

云风在博客中提到了另外一种lua实现面向对象的做法:

local _class={}
 
function class(super)
	local class_type={}
	class_type.ctor=false
	class_type.super=super
	class_type.new=function(...) 
			local obj={}
			do
				local create
				create = function(c,...)
					if c.super then
						create(c.super,...)
					end
					if c.ctor then
						c.ctor(obj,...)
					end
				end
 
				create(class_type,...)
			end
			setmetatable(obj,{ __index=_class[class_type] })
			return obj
		end
	local vtbl={}
	_class[class_type]=vtbl
 
	setmetatable(class_type,{__newindex=
		function(t,k,v)
			vtbl[k]=v
		end
	})
 
	if super then
		setmetatable(vtbl,{__index=
			function(t,k)
				local ret=_class[super][k]
				vtbl[k]=ret
				return ret
			end
		})
	end
 
	return class_type
end

这里有个小小的优化,就是把访问过的字段缓存到vtbl中,避免频繁调用元方法。

如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路-

posted @ 2021-05-19 00:01  异次元的归来  阅读(117)  评论(0编辑  收藏  举报